SwiftUI select only XX first value in array with ForEach - arrays

In my project I've got a view where I use a ForEach to load each line of my structured array. This array is filled via a Firebase download when the app is started but to be more responsive I would like to load only some last value. I've got a date value in each line and I would like for example the only 10 first lines where the first line is the most recent entry !
The number of last entry to show is saved via the #AppStorage in the user setting part of the app.
And I'm trying to implement a button "load more line" at the end of all my array lines.
My view is defined as follow :
//get the numbers of flights to load in my array or 10 by default
#AppStorage("flightToLoad") private var flightToLoad: Int = 10
#State var thisSessionFlightToLoad = flightToLoad
[...var body ...]
ScrollView{
VStack {
//First ForEach to set the Date value as Section Header :
ForEach(flightLibrary.uniqueDates, id: \.self) { date in
Section(header:
HStack{
Text(flightLibrary.frmt.string(from: date))
.font(.title3)
Spacer()
}
) {
//Second ForEach to show all the value of each line in my array
ForEach(flightLibrary.testFlight.filter({Calendar.current.isDate($0.date, inSameDayAs: date)})) { flight in
if flight <= thisSessionFlightToLoad{
ZStack {
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(Color.white)
NavigationLink(
destination: {
// Code for destination view
}, label: {
LogbookCellView2(flight: flight)
}).accentColor(.black)
}
}else{
// Here I try to add a Button after the line allready loaded
Button{
thisSessionFlightToLoad = thisSessionFlightToLoad + 10
}label:{
Text("Load more flights")
}
}
}
}
}
}.padding(.horizontal, 8)
}
But this is totally not working ...
Hope to be clear in my question !
Thanks for your help !

Related

AppStorage - Array - Error: No exact matches in call to initializer

I am developing an App for increasing productivity. My Main Goal in this file is to add Views over a dialog. Another target is to save the data in an Array for using it again with the annotation #AppStorage.
struct Task : Identifiable {
var id = UUID()
var myContent = "Empty"
var myCounter = 0
}
I'm using this struct to save my data which is here mainly the tasks name.
struct TaskView : View {
var task : Task
var body: some View {
HStack {
Spacer()
Text(String(task.myContent) ?? "test")
Spacer()
Text("Sessions today: " + String(task.myCounter))
Spacer()
Image(systemName: "xmark.bin.fill")
}
}
}
For displaying the data I'm using my own struct.
struct ItemList: View {
#AppStorage("myviews") var myviews : [Task]? = nil
#State private var showingAlert = false;
#State private var taskName = "tim";
var body: some View {
VStack{
if(!myviews.isEmpty){
for task in myviews {
TaskView(task: task)
}
}
Spacer()
Button {
showingAlert = true;
} label: {
Image(systemName: "plus")
.padding()
.background(Color.red)
.accentColor(.white)
.cornerRadius(100)
}
.alert(isPresented: $showingAlert) {
Alert(title: Text("Important message"), message: TextField("Task: "; text: $taskName), primaryButton: .destructive(Text("Got it!")){
myviews.append(Task(myContent: String(taskName), myCounter: 0))
})
}
Spacer()
}
}
}
So the main part consists of my #AppStorage Array, a loop to show existing "tasks" and a Dialog to add these tasks to the array.
The Error I am getting is the "No exact matches in call to initializer" directly in the line #AppStorage("myviews") var myviews : [Task]? = nil
I already tried different variations of initializing the array until I read in a forum that leaving the initialization not optional could be a cause to my problems.
Furthermore I checked my "Text" - Fields for the wrong types and casted the Int's (myCounter) to String.
It feels like I read every StackOverflow Article regarding my Error but none could help me.

Fatal error: Index out of range while animating screens

I am running into an error:
Fatal error: Index out of range
I would like to know what is out of range since I have 6 values printed in the console and 6 in my JSON that I created. This is happening when I am navigating to the last item.
(I already tried similar questions/answers from StackoverFlow with no success).
enter image description here
`
import SwiftUI
public struct Stepper: View {
public enum Step: Equatable {
case fixed, animated(duration: Int)
}
#Binding var selected: Int
let steps: [Step]
#State var timer: Timer?
public init(selected: Binding<Int>, steps: [Step]) {
self._selected = selected
self.steps = steps
}
public var body: some View {
HStack {
ForEach(steps.indices) { item in
StepView(step: steps[item], index: item, selected: $selected)
}
}
.padding(.horizontal,16)
.padding(.vertical,12)
.onChange(of: selected,perform: updateTimer)
.onAppear{
updateTimer(newValue: 0)
}
}
func updateTimer(newValue: Int) {
timer?.invalidate()
guard case .animated(let duration) = steps[newValue] else {
return
} **// the app is crashing here in the guard let **
timer = Timer.scheduledTimer(withTimeInterval: Double(duration),
repeats: false,
block: { _ in
if steps.count > selected + 1 {
withAnimation {
selected += 1
}
}
})
}
}
`
I already tried to update the timer with different values (newValue: ) and to pass item instead of newValue with no success.
ForEach isn't a for loop, it's a View that needs to be supplied with Identifiable data (so it can track changes), e.g.
ForEach(steps) { step in
How often and in what order the closure is called depends on what kind of subviews are used in it.
You also need #State var steps and struct Step: Identifiable

Loop through entity array in SwiftUI to build tree-structured view based on values in array data

I'm totally new to Swift. I need to traverse an array, and based on what the data is, build out a custom tree structure. I would share my code but it's too bulky and too horrible to be seen in public. It's not even close to working. So here's some pseudocode instead:
pass in an array of ScheduleCell models (data for a scheduled tv program) to a SwiftUI View.
Each ScheduleCell contains:
Date of broadcast
Channel
Starttime
Program title, subtitle, etc
in the view body, loop over each ScheduleCell
if ScheduleCell is new day then
display new date
go down one line
end
if ScheduleCell is new channel then
display ScheduleCell.channel but do NOT drop down a line
end
display ScheduleCell.title, also on same line as channel
Keep going on the same horizontal line till you hit the next channel
end
The end effect should be like this:
11-16-2021
CNN News Program 1 News Program 2 News Program 3 Etc.
NBC Late Show Infomercial Barney Miller Rerun. Etc.
11-17-2021
CNN News Program 1 News Program 2 News Program 3 Etc.
NBC Late Show Infomercial Barney Miller Rerun. Etc.
It seems like it should be really simple to do this. I've tried setting up a tree structure, but I'm having trouble with the SwiftUI View protocol. I have tried building the tree structure in a function outside of the view body, but I'm having trouble figuring out how to get that structure into the view body.
In the alternative, I've backed off all that to just trying to get it to work via brute force inside the view by tracking the array index, like this:
struct ScheduleDisplayView: View {
var schedule: [SchedSlot]
let dfdo = DateFormatter.dateOnly
let dfto = DateFormatter.timeOnly
let chanwidth: CGFloat = 45.0
let fontsize: CGFloat = 10.0
var body: some View {
List {
ForEach(schedule.indices,id:\.self) { idx in
let ssDay: String = dfdo.string(from: schedule[idx].startTime!)
let ssChan: Int32 = schedule[idx].channel!.channelID
if idx == 0 ||
ssDay != dfdo.string(from: schedule[idx-1].startTime!)
{
VStack {
Text(ssDay)
.frame(maxWidth: 200 + chanwidth, alignment: .leading)
}
}
HStack {
if idx == 0 ||
ssChan != schedule[idx-1].channel!.channelID
{
VStack {
Text(String(schedule[idx].channel!.channelID))
.frame(maxWidth: chanwidth, alignment: .center)
//.font(.system(size: fontsize))
Text(schedule[idx].channel!.callSign!)
.frame(maxWidth: chanwidth, alignment: .center)
//.font(.system(size: fontsize))
}
}
Text(schedule[idx].program!.title!)
.frame(maxWidth: 200, alignment: .leading)
.border(Color.black)
}
}
}
}
}
But the above approach isn't working because the HStack can't keep the program titles on the same line.
Thanks in advance for any input.
Additional material:
Here's a few random slices of the CoreData entities this is based on:
SchedCell
stationID starttime programID duration endtime isUsed channel program
34916 2021-09-29 19:09:00.000 EP000045050088 PT00H09M 2021-09-29 19:18:00.000 0
12131 2021-09-29 19:15:00.000 EP022222100024 PT00H15M 2021-09-29 19:30:00.000 0
34916 2021-09-29 19:18:00.000 EP000045050208 PT00H09M 2021-09-29 19:27:00.000 0
Program
series programID title subtitle fulldescription genre isUsed
EP00000066 EP000000660001 A Pup Named Scooby-Doo Night of the Living Burger After a quarrel, a burgerlike creature haunts Shaggy and Scooby. Children 0
EP00000066 EP000000660002 A Pup Named Scooby-Doo For Letter or Worse The ghost of a long-dead gangster haunts a TV studio. Children 0
EP00000066 EP000000660003 A Pup Named Scooby-Doo A Bicycle Built for Boo! A green monster steals Shaggy's bike. Children 0
EP00000066 EP000000660004 A Pup Named Scooby-Doo The Baby Sitter From Beyond The baby sitter for Shaggy's little sister appears to be a monster. Children 0
Channel
stationID callSign fullName channelID isUsed
15722 WCIX WCIX 2 0
11345 WCIA WCIA 3 0
11278 WAND WAND 4 0
10685 KSDK KSDK 5 0
10269 HSN Home Shopping Network 6 0
11824 WRSP WRSP 7 0
11069 QVC QVC 8 0
As for code samples of my attempts to build the tree structure outside of the view, I have no working code. It's all just fragments that produced a variety of error messages. Here are the node structures. I'm still working out a routine to assemble them into a working tree, will post that as soon as I have something worth looking at:
class RootNode {
var children: [DayNode] = []
func add(child: DayNode) {
children.append(child)
child.parent = self
}
}
class DayNode {
var parent: RootNode
var date: String
var children: [ChannelNode] = []
init(date: String) {
self.date = date
}
func add(child: ChannelNode) {
children.append(child)
child.parent = self
}
}
class ChannelNode {
var parent: DayNode
var channel: String
var children: [SchedSlot] = []
init(channel: String) {
self.channel = channel
}
func add(child: SchedSlot) {
children.append(child)
//child.parent = self
}
}
As I mentioned in the comments, I think this is more of a data organization/sorting issue than a SwiftUI layout issue. If you data is grouped and sorted correctly, the layout becomes more trivial (eg you don't have to try to decide whether to break a line because of a new channel).
In the following example, I spend the bulk of the code grouping and sorting the data. Then, the layout itself is relatively simple.
It's important to note that I do a couple of unsafe things here for brevity, like using first! that you would want to test for or have contingencies for in real code.
struct SchedCell {
var stationID: Int
var startTime: Date
var programID: String
var channel: String
}
func generateSampleCells() -> [SchedCell] {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
return [
("2021-09-29 19:09:00.000",34916,"EP000045050088","PT00H09M"),
("2021-09-29 19:09:00.000",34917,"EP000045050088","PT00H09M"),
("2021-09-29 19:15:00.000",12131,"EP022222100024","PT00H15M"),
("2021-09-29 19:18:00.000",34916,"EP000045050208","PT00H09M"),
("2021-09-30 19:09:00.000",34916,"EP000045050088","PT00H09M"),
("2021-09-30 19:15:00.000",12131,"EP022222100024","PT00H15M"),
("2021-09-30 19:18:00.000",34916,"EP000045050208","PT00H09M"),
("2021-09-30 19:15:00.000",12132,"EP022222100024","PT00H15M"),
("2021-09-29 19:09:00.000",4916,"EP000045050088","PT00H09M"),
("2021-09-29 19:09:00.000",4917,"EP000045050088","PT00H09M"),
("2021-09-29 19:15:00.000",2131,"EP022222100024","PT00H15M"),
].map {
SchedCell(stationID: $0.1, startTime: formatter.date(from: $0.0)!, programID: $0.2, channel: $0.3)
}
}
struct ContentView: View {
private var data = generateSampleCells()
private var formatter = DateFormatter()
struct ScheduleDay {
var dateStamp: String
var date : Date
var channels: [ChannelLineup]
}
struct ChannelLineup {
var channelName: String
var programs: [SchedCell]
}
struct DateStampedSchedCell {
var dateStamp: String
var cell: SchedCell
}
var sortedData : [ScheduleDay] {
formatter.dateFormat = "MM-dd-yyyy"
let dateStamped = data
.map { item -> DateStampedSchedCell in
DateStampedSchedCell(dateStamp: formatter.string(from: item.startTime), cell: item)
}
let days = Dictionary(grouping: dateStamped, by: { $0.dateStamp} ).values
let channelMappedDays = days.map { day in
ScheduleDay(dateStamp: day.first!.dateStamp,
date: day.first!.cell.startTime,
channels: Dictionary(grouping: day, by: { $0.cell.channel }).map { ChannelLineup(channelName: $0.key, programs: $0.value.map(\.cell))}
)
}.sorted(by: {a,b in a.date < b.date})
return channelMappedDays
}
var body: some View {
ScrollView(.horizontal) {
ForEach(sortedData, id: \.dateStamp) { day in
VStack(alignment: .leading) {
Text("Day: \(day.dateStamp)")
.bold()
ForEach(day.channels, id: \.channelName) { channel in
HStack {
Text("Channel: \(channel.channelName)")
.foregroundColor(.red)
ForEach(channel.programs, id: \.programID) { program in
Text(program.programID)
}
}
}
}.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
}

SwiftUI watchOS How to display array content as subviews in a single View?

I'm doing an Apple Watch app, using SwiftUI. The idea is to display different activity content in a single watch ContentView using four individual subviews within the ContentView Body View. I don't want to use a List to display the activity content, but rather multiple custom views within the ContentView.
I would like each individual subview to display unique content from my model.
My model is called QuadActivity and has the following content:
struct QuadActivity: Identifiable {
let id = UUID()
var activityImage: Image
var activityTitle: String
}
I currently have created an extension to QuadActivity as follows, to hold some hardcoded test data:
extension QuadActivity {
static func all() -> [QuadActivity] {
return [
QuadActivity(activityImage: activityImage1, activityTitle: "activity1"),
QuadActivity(activityImage: activityImage2, activityTitle: "activity2"),
QuadActivity(activityImage: activityImage3, activityTitle: "activity3"),
QuadActivity(activityImage: activityImage4, activityTitle: "activity4")]
}
}
My ContentView.swift Body view is made up of a VStack with 2 HStacks embedded. Each HStack contains 2 of my subviews with miscellaneous spacers and padding modifiers. Each of the subviews should display the content of one of the array elements from an instance property:
var activityArrayEntry = QuadActivity.all()
Thus HStack 1 should display activityImage1 and activity1 and activityImage2 and activity2. The other HStack should display the array elements for items 3 and 4.
I can't figure out how to access each of the activityArrayEntry array elements and display each one in one of the subviews.
I was thinking I could use a:
ForEach(activityArrayEntry) { activity in
VStack and embedded HStack code here}
and display the subview content by looping through the ForEach above.
However, since all my VStack and HStack and subview code is within the ForEach loop, the same array element activity content would be displayed for all subviews because the loop encompasses all the view information for a single pass of the loop. I want each subview to display one of the unique array element's content.
If I move the ForEach within the ZStack and HStack code for each HStack subview display section, it will loop through the array entries, but the loop won't encompass all the subviews code and I won't get all the subviews to display only the activity array content from 1 array element.
Maybe using a ForEach loop is not the way. Is there another way to access the individual array elements from my instance variable such that each unique array element is used only in 1 of the subviews?
Again, how do I get the overall ContentView.swift to display the four subviews within the ZStack and HStack structure so that each subview displays the activity content of only 1 of the array elements.
Here is my ContentView so far. Note a number of commented lines that I will eventually comeback to in order to use an Observed Object model approach from a #Published Observable Object of my model. This will eventually be (maybe) the approach instead of the function all() from my model that I'm using now with hardcoded data to test the data flow in my app... thus the original question/issue.
Note
Call to QuadView() is just a call to an extracted subview where I define the display of the subview (simple Image and Text):
import SwiftUI
struct ContentView: View {
// #ObservedObject var quadViewVM = QuadViewVM()
var activityArrayEntry = QuadActivity.all()
var body: some View {
ZStack {
HStack {
Divider()
.frame(height: 175.0)
}
.edgesIgnoringSafeArea(.horizontal)
ForEach(activityArrayEntry) { activity in
VStack {
HStack(alignment: .center) {
QuadView(activityTitle: "\(activity.activityTitle)", activityImage: activity.activityImage)
// NavigationLink(destination: QuadDetail(content: , newActivity: false)) {
//
// }
.frame(width: 85.0, height: 100.0)
.buttonStyle(PlainButtonStyle())
Spacer()
QuadView(activityTitle: "\(activity.activityTitle)", activityImage: activity.activityImage)
// NavigationLink(destination: QuadDetail()) {
//
// }
.frame(width: 85.0, height: 100.0)
.buttonStyle(PlainButtonStyle())
}
.padding(.horizontal, 15.0)
.padding(.bottom, -10.0)
Divider()
HStack(alignment: .center) {
QuadView(activityTitle: "\(activity.activityTitle)", activityImage: activity.activityImage)
// NavigationLink(destination: QuadDetail()) {
//
// }
.frame(width: 85.0, height: 100.0)
.buttonStyle(PlainButtonStyle())
Spacer()
QuadView(activityTitle: "\(activity.activityTitle)", activityImage: activity.activityImage)
// NavigationLink(destination: QuadDetail()) {
//
// }
.frame(width: 85.0, height: 100.0)
.buttonStyle(PlainButtonStyle())
}
.padding([.leading, .bottom, .trailing], 15.0)
.padding(.top, -10.0)
.padding(.top, 30.0)
.edgesIgnoringSafeArea(.horizontal)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Instead of static QuadActivity.all() you can store your data in the ViewModel:
class QuadViewVM: ObservableObject {
#Published var quadActivities: [QuadActivity] = [
QuadActivity(activityImage: activityImage1, activityTitle: "activity1"),
QuadActivity(activityImage: activityImage2, activityTitle: "activity2"),
QuadActivity(activityImage: activityImage3, activityTitle: "activity3"),
QuadActivity(activityImage: activityImage4, activityTitle: "activity4"),
]
}
In your ContentView you can create a grid by using two ForEach loops (as it's a 2D grid):
struct ContentView: View {
#ObservedObject var quadViewVM = QuadViewVM()
let columnCount = 2
var rowCount: Int {
quadViewVM.quadActivities.count / columnCount
}
var body: some View {
ZStack {
// Horizontal divider
VStack {
Divider()
}
.edgesIgnoringSafeArea(.horizontal)
// Vertical divider
HStack {
Divider()
}
.edgesIgnoringSafeArea(.vertical)
activityGrid
}
}
var activityGrid: some View {
VStack {
ForEach(0 ..< self.rowCount) { row in
HStack {
ForEach(0 ..< self.columnCount) { column in
self.quadView(row: row, column: column)
}
}
}
}
}
func quadView(row: Int, column: Int) -> some View {
let activity = quadViewVM.quadActivities[row * columnCount + column]
return QuadView(activity: activity)
.frame(width: 85.0, height: 100.0)
.buttonStyle(PlainButtonStyle())
}
}
The QuadView is extracted to another function to make it more readable and easier to apply view modifiers.
I'd also recommend passing the whole QuadActivity variable to the QuadView (instead of its single components) - specifically when you need them all:
struct QuadView: View {
let activity: QuadActivity
var body: some View {
...
}
}

Speed Up a list

I'm try to speed up my APP that for now unfortunately is very slow on performing some search and list of data with Swift Ui.
first of all I have a data model that describe my object airport, called AirportModel
import Foundation
class AirportModel : Identifiable , Codable{
var aptICAO : String
var aptName : String
var aptCity : String
var aptCountry :String
var aptIATA : String
init(aptICAO: String, aptName: String, aptCity: String, aptCountry: String, aptIATA: String) {
self.aptICAO = aptICAO
self.aptName = aptName
self.aptCity = aptCity
self.aptCountry = aptCountry
self.aptIATA = aptIATA
}
}
I have a local file apt.json, that contain all the data information for my airport (I downloaded from internet this json, inside there are around 29000 airport)
so when I run the first time the app, with the following function , i create and save the airportVector of type AirportModel.
func openfilejson (fileName : String) {
if let path = Bundle.main.path(forResource: fileName, ofType: "json") {
do {
let fileUrl = URL(fileURLWithPath: path)
let datafile = try Data(contentsOf: fileUrl,options: .mappedIfSafe)
let json = JSON(data:datafile)
objectWillChange.send()
for (key,_) in json {
let airport = AirportModel(aptICAO: "", aptName: "", aptCity: "", aptCountry: "", aptIATA: "")
airport.aptName = json[key]["name"].stringValue
airport.aptCity = json[key]["city"].stringValue
airport.aptCountry = json[key]["country"].stringValue
airport.aptIATA = json[key]["iata"].stringValue
airport.aptICAO = json[key].stringValue
airportVector.append(airport)
}
debugPrint("nel vettore airport Vector ci sono \(airportVector.count) aeroporti")
save2() // to save airport vector in the plistfile
debugPrint("SALVATO IN MEMORIA ")
} catch {
print("ERRORE OPEN FILE AEROPORTI")
}
}
}
all, this work fine , the vector with 29000 airport inside is created once the app is ru the first time.
NOW THE BIG ISSUE.
in one view, I try to list this airport using SwiftUI and a searchBar to search on it.
the problem is, due to the big amount of data to be opened, when I load the View with the list; the data take very long to be listed and moreover when I lunch the search everything is stuck!
any idea how can I process or speed up this huge amount of data to avoid the app stuck on the loading of the following view!
import SwiftUI
struct ContentView: View {
#ObservedObject var dm: DataManager
#State private var searchTerm : String = ""
var body: some View {
VStack {
Text("List")
SearchBar(text: $searchTerm).shadow(radius: 10)
List(dm.airportVector.filter{
$0.aptCity.localizedCaseInsensitiveContains(searchTerm)
|| $0.aptName.localizedCaseInsensitiveContains(searchTerm)
}){ item in
HStack {
Text(item.aptICAO).bold().font(Font.system(size:20)).frame(width: 90, height: 10)
//
Text(item.aptName).font(Font.system(size: 15))
Spacer()
VStack(alignment: .leading) {
Text(item.aptCity).font(Font.system(size: 10)).font(.subheadline)
Spacer()
Text(item.aptCountry).font(Font.system(size: 10))
}
}
}
}
}
}
Thanks in advance.
Damiano
Make your data conform to Identifiable and use ForEach inside of your List with a separate View to render the item:
List {
ForEach(items) {
MyItem(item)
}
}
Only render items when they change by making MyItem conform to Equatable, and define the == function. Make sure the == function is being executed by adding a breakpoint or print statement. If it's not being executed see this article for a workaround:
https://swiftui-lab.com/equatableview/
If the above suggestion works, but you find yourself making wholesale modifications to the list after initial render, try using id() during these modifications. See https://swiftui-lab.com/swiftui-id/
If the above suggestions don't improve things enough, you probably need a more custom solution. Use a TableView or CollectionView, see https://github.com/apptekstudios/ASCollectionView

Resources