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

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)
}
}

Related

SwiftUI ForEach.onDelete properly animates but then resets

Trying to figure out why my ForEach loop isn’t updating. I have the model marked as ObservedObject and have done everything I could to make sure the updates were happening. I even saw that the model was being updated while printing.
class Model {
var array: [Int]
}
…
struct ModelView: View {
#ObservedObject var model: Model
var body: some View {
List {
ForEach(model.array.indices, id:\.self) { index in
…
}.onDelete(perform: delete)
}
}
The row is animating and acting like it is deleting and does delete in the model, however the deleted row animates back in and the original data set is shown!
I figured it out. I needed to make the array in my model object with the #Published property wrapper. Doing that fixed everything!

SwiftUI – Xcode not building when using the index variable of a ForEach loop

I have a ForEach loop inside a TabView. Whenever I try to build the project using the index (for lack of a better word, here: slide) variable in a condition, it stalls and is not building; it's not showing any errors. If I'm using it interpolated, it works/builds.
TabView {
ForEach(1...3, id: \.self) { slide in
HStack {
if slide == 1 {
Text("Do something if slide is 1st slide") // doesn't work
}
Text("Test \(slide)") // works
}
.tag(slide)
}
}
Thanks for any tips!
Edit: Deleted case 2)
Edit: Added Tabview

Animate Change in SwiftUI ForEach Grid Using Array Indices

Does anyone know how I can animate a sorting change to grid items while iterating off of the array index in a ForEach?
I am showing a grid of items (LazyVGrid) from an array and the user can change the sort order of these items. When the sort order changes, I'd like to animate the change in the grid. This would work great if I were to use code similar to the following:
Button {
withAnimation {
noteModel.sortMethod = noteModel.sortMethod.next()
}
} label: {
Text("Change Sort Order")
}
ScrollView {
LazyVGrid(columns: columns) {
ForEach(self.notes, id: \.self) { note in
VStack {
Text(note.title)
Text(note.body)
}
.padding()
}
}
}
However, if my ForEach iterates off of the array index the change does not animate. So it won't animate if my code is similar to this:
Button {
withAnimation {
noteModel.sortMethod = noteModel.sortMethod.next()
}
} label: {
Text("Change Sort Order")
}
ScrollView {
LazyVGrid(columns: columns) {
ForEach(self.notes.indices, id: \.self) { idx in
VStack {
Text(self.notes[idx].title)
Text(self.notes[idx].body)
}
.padding()
}
}
}
Why don't I just do it the first way? The reason is because I found a great resource for dynamically adjusting frame sizes for items inside a grid (found here: https://swiftui-lab.com/impossible-grids/), but that requires the use of array indices in order to work. It's pretty cool and I am hoping I don't have to choose one or the other (dynamic sizing or animate changes).
Thoughts or ideas would be greatly appreciated. Thanks!
ForEach in SwiftUI isn’t the same as a for loop. Your data needs to be identifiable for structural identity to work and you certainly should not be using indices. Either implement the Identifiable protocol or tell ForEach what key path is a unique ID (or is a getter that creates one from other properties) in the item struct. Don’t ever use id:\.self.
You can learn more about structural identity in the video WWDC 2021 Demystify SwiftUI

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.

Why is my SwiftUI List row not always updating internally

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 {
}
}
}