Why is my SwiftUI List row not always updating internally - reactive-programming

I have a list of reminders grouped into sections by completion and date. With data coming from an ObservedObject DataStore called global. I pass a realmBinding to the cell. The cell can update this binding and it will trigger the data store to update.
List {
// Past Due
if self.global.pastDueReminders.count > 0 {
Section(header: SectionHeader {}){
ForEach(self.global.pastDueReminders) { reminder in
NavigationLink(destination: ReminderDetail( reminder: reminder.realmBinding())) {
GeneralReminderCell(reminder: reminder.realmBinding())
}
}
}
}
// Completed
if self.global.completeReminders.count > 0 {
// Same as PastDue but for Completed
}
}
The cell looks something like:
struct GeneralReminderCell: View {
#Binding var reminder:Reminder
var body: some View {
HStack(alignment:.top, spacing: 10) {
Image(systemName: reminder.completed ? "checkmark.circle.fill" : "circle")
.onTapGesture(perform:{ self.reminder.completed = !self.reminder.completed })
VStack(alignment: .leading, spacing: 2) {
Text("Follow up with \(reminder.client.fullName)").fontWeight(.semibold)
if reminder.title.count > 0 {
Text(reminder.title)
}
Text(reminder.date.formatted()).foregroundColor(.gray)
}
}.padding(.vertical, 10)
}
}
When tapping on an image it toggles the reminder completion state and its position changes in the List view. The image that was tapped should changed to a filled in check when completed.
This behaviour almost always happens as expected, but sometimes the checked image will get out of sync with the completed state of reminder. I've look at this for quite some time and have not made much headway. Why is the checked image not always matching the state of the data?

Even though this is a very old question, I may have been working on what appears to be this same problem. In my case, my App is running on macOS. At first, the problem also seemed to be very intermittent and had been difficult to reproduce consistently.
I also have a View with a ForEach supplying rows to a List. My row's View contains an #State for an Optional Image that gets updated several different ways via actions performed by that same row View (e.g. Continuity Camera, file picker or drag & drop). The issue is that sometimes the new Image is shown and sometimes it is not shown. Using Self._printChanges() I am able to see that the #State is changing and the row's body it is being redrawn, however, the rendered View does not change. The only pattern that I am able to observe is that this issue only seems to occur with the last row in the List. Based on the success of my workaround below, it seems to confirm that there is an issue with the way SwiftUI's List reuses table cells.
My solution/workaround is to replace:
List {
ForEach {
}
}
With:
ScrollView {
LazyVStack {
ForEach {
}
}
}

Related

SwiftUI: How to Disable List Scrolling on Reusable View

I've got an interesting glitch in SwiftUI -
I'm using a List as part of my interface that is intended to be un-scrollable. This is how I've designed it:
List {
ForEach(myArr) {
...
}
}
.listStyle(.plain)
.onAppear {
UIScrollView.appearance().isScrollEnabled = false
UIScrollView.appearance().showsVerticalScrollIndicator = false
}
Now, when I first load the view containing this list, it works perfectly; you cannot scroll this list. However, if I navigate to a different page in my application and then back to the page containing this list (which creates a new instance of the view), scrolling becomes enabled.
How can I fix this?
Use .disabled(true) on the list to disable interaction with the list.

SwiftUI array.isEmpty do not show until received array from Firestore

Currently have a VStack populating with items from firestore and am trying to display a message if there are no items in the db. It works perfectly but there is a brief second where it shows the "No Items" message before loading the list of items. I understand it simply hasn't gotten the results yet from the db. Is there any way to hold off showing ANYTHING until that call is complete?
VStack(alignment: .leading){
if itemList.isEmpty {
Text(โ€œNo itemsโ€)
} else {
ForEach(itemList.indices, id: \.self) { i in
Text(itemList[I].item)
}
}
}
.onAppear() { self.readItems() }
No, you can hold rendering until there is data; that might take way too long. Your code has to always render something, no matter what state the app is in.
But what you can do is set a variable to signal whether data has been loaded, like isLoaded, set it initially to false, set it to true in the completion handler, and use that to determine what to show. Most commonly, you'll show a single Text view Loading... in that case, and then switch over to what you now have once isLoaded is true.

Selecting a picker value to lead to a text field, SwiftUI

Im trying to implement a feature in my app.
When I click on my picker:
Picker(selection: $profileViewModel.education,
label: Text("Education Level")) {
ForEach(Education.levels, id: \.self) { level in
Text(level).tag(level)
}
}
This takes me to a screen and then I select the value (this is fine - it works as expected)
How could I select the value which then takes my to let's say another screen so I can fill in more details regarding the selected value.
For example the above picker has a values to select eduction level, after selecting it, how could I get an action sheet/another screen appear so I can have a text field there to save this extra data to or once the selection is made, a text field appears for me to save some extra data, and then clicking a button which would take me to the original screen of the picker (hope that makes sense)?
I've tried researching online for a problem similar to this but can't seem to find one/or if you can point me in the direction of what I should be looking into?.
Tried the following:
If I correctly understood your scenario here is a possible approach (replication tested with Xcode 12 / iOS 14)
Picker(selection: $profileViewModel.education,
label: Text("Education Level")) {
ForEach(Education.levels, id: \.self) { level in
Text(level).tag(level)
}
}
.onChange(of: profileViewModel.education) { _ in
DispatchQueue.main.async {
self.showSheet = true
}
}
.sheet(isPresented: $showSheet) {
// put here your text editor or anything
Text("Editor for \(profileViewModel.education)")
}

SwiftUI - View Not Registering Input Data On Change

I created a View that displays a line graph based on an array of Doubles. The graph struct then divides the view horizontally in order for each of the points to have the same amount of separation. I am using GeometryReader and Path to make the graph. When I load the View with a given array of Doubles it displays correctly.
However, when the data changes, such as adding or removing data, the graph is not updated properly.
As an example using text, this is what the graph looks like normally (Assume it is a line graph): |-_-_-_-_|
When I add a value to the array of Doubles, the graph does appear to show that a new value was added, but the value is not recognized or read. This is what it looks like when a value is added (Again, assume it is a line graph): |-_-_-_-_ |. As you can see, it recognized there is a new value because it allocated space for the new value, but no line was shown, as it most likely does not recognize the added value. When I reload the view, such as by going back and into the view again, the graph is displayed correctly.
The big problem comes when a value is deleted. When a value is deleted, the app crashes with the error Fatal error: Index out of range. When I load the app again the graph is fixed.
I thought there might be something wrong with my deleting, so I created a test file in which I referenced an array of Doubles and buttons to add and remove values from the array, and the same behavior happened. This showed that the values put into the View were not updated properly.
This is the file I created to test the behavior of my graph:
struct GraphTest: View {
/*Data*/
#State var data: [Double] = [180, 242, 164, 202, 176, 100]
var body: some View {
VStack {
/*Struct that shows the line graph*/
LineGraph(data: data)
.frame(height: 200)
/*Button to remove the last item of array*/
Button(action: {
data.removeLast()
}) {
Text("Delete")
}
/*Button to add a value to the end of the array*/
Button(action: {
data.append(180)
}) {
Text("Add")
}
}
}
}
And this is how I'm setting up my LineGraph struct:
struct LineGraph: View {
var data: [Double]
var body: some View {
GeometryReader { geometry in
...
I tried doing data #Bindable in LineGraph, but I could not find a way to do so, as I'm getting the data from a Core Data FetchRequest, but I really do not know what to do from now to fix the issue, as I am relatively new in SwiftUI, and could not find anything similar on the internet.
Any help is appreciated, thank you :)
There is not enough code provided in LineGraph, but I assume you have inside something like
ForEach(0..<data.count) { i in // iterating this way result in crash !!
Text("\(self.data[i])")
}
instead you have to iterate at least like
ForEach(data.indices, id: \.self) { i in // no crash !!
Text("\(self.data[i])")
}

SwiftUI re-render code inside for loop after #State chage

I wrote a small program in this gist https://gist.github.com/alfonsodev/411622f65727bd5f44eff13f6e7e0a9b
By pressing the buttons you can change the value of playlistIndex correctly and the UI is refreshed.
On the contrary, the loop that prints each item of the playlist does not update which one is selected, and it is using the same variable playlistIndex
ForEach(0..<playlist.count) {
if $0 == self.playlistIndex {
Text("๐Ÿ”ˆ \(playlist[$0].title)")
} else {
Text(playlist[$0].title)
}
}
I was expecting ForEach to run every time #State changes, it seems that it run the first time and hold the first value of playlistIndex.
My question is, how can I print #State dependent loops ?
ForEach does not refresh, because range was not changed - SwiftUI tries to optimise redrawing by checking for equality...
Use instead like below (if your playlist item Identifiable)
ForEach(Array(playlist.enumerated()), id: \.element) { (i, item) in
if i == self.playlistIndex {
Text("๐Ÿ”ˆ \(item.title)")
} else {
Text(item.title)
}
}