ForEach Breaks Data Binding

Using a ForEach loop breaks my data binding to an object in an array. I can create my view using the index of zero - array[0] - and it responds to the databinding appropriately. If I wrap it with a ForEach but do not change the index of zero to an iterator - array[0] NOT array[index], it still creates the view, but now the data binding does not work. Same exact code, just wrapped in a ForEach loop that should only loop once (the array only has one object inside).

// Does data bind correctly - UI is updated
if(self.noise.twoControlEffects[0].isDisplayed){
                     TwoControlTemplate(title: "Low Pass Filter",
                         isBypassed: self.$noise.twoControlEffects[0].isBypassed,
                         knobModel1: self.$noise.twoControlEffects[0].control1,
                         knobModel2: self.$noise.twoControlEffects[0].control2)
}

// Does not update UI
ForEach(noise.twoControlEffects.indices){ index in
                 if(self.noise.twoControlEffects[0].isDisplayed){
                     TwoControlTemplate(title: "Low Pass Filter",
                         isBypassed: self.$noise.twoControlEffects[0].isBypassed,
                         knobModel1: self.$noise.twoControlEffects[0].control1,
                         knobModel2: self.$noise.twoControlEffects[0].control2)
     }
}


I tried it with array[index] as well, but then did the above once it was not working.

// Does not update UI
ForEach(noise.twoControlEffects.indices){ index in
                 if(self.noise.twoControlEffects[index].isDisplayed){
                     Spacer()
                     TwoControlTemplate(title: "Low Pass Filter",
                         isBypassed: self.$noise.twoControlEffects[index].isBypassed,
                         knobModel1: self.$noise.twoControlEffects[index].control1,
                         knobModel2: self.$noise.twoControlEffects[index].control2)
     }
}


What does the ForEach loop do that breaks databinding? How can I create and update views using an array of objects?

Accepted Reply

The ForEach View cannot not work with indicies for dynamic data, it requires identifiers which is why we supply it with an array of Identifiable data.

This is so that it can calculate inserts, moves and deletions which obviously is impossible with an array of indicies which will in the case of moving 5 items around the indices will still be 0-4.

Replies

UPDATE - right after posting this I found a solution (See adding HStack comments at the end of discussion):

https://forums.developer.apple.com/message/412141#412141


So, this code works and I have no idea why:

ForEach(noise.twoControlEffects.indices){ i in
     VStack{
          if(self.noise.twoControlEffects[i].isDisplayed){
               TwoControlTemplate(title: "Low Pass Filter",
                                   isBypassed: self.$noise.twoControlEffects[i].isBypassed,
                                   knobModel1: self.$noise.twoControlEffects[i].control1,
                                   knobModel2: self.$noise.twoControlEffects[i].control2)
          }
     }
}


Apparently, you just have to wrap the inside of your ForEach code in a HStack or VStack.

  • This was not the solution, my view is already inside of a HStack, what did the solution to me was the ForEach not using a range. I think when you use a range, where its 0..<(some bindingvar).count) the range is not updated on struct, however the other constructor of ForEach that you used.. ForEach([Hashable]), it will update.

Add a Comment

Jeez... I could have looped infinitely without your suggestion... Thanks, it does work indeed, and it saves my evening...

Is anyone from Apple planning on addressing this issue? It seems to me that control statements in general are breaking data binding. And this in my opinion is a show stopper.

Apple... Please the least that you can do, even if you don't want to fix it, or feel it's "as design" is shed some light on the problem and give us an explanation so we understand this better.
Post not yet marked as solved Up vote reply of abzy Down vote reply of abzy

It seems to me that control statements in general are breaking data binding. And this in my opinion is a show stopper. 

I'd love to explore this further with you if you're still around, abzy! There are some gotchas to working with control statements, especially in situations like this, where the types of views you are passing in get involved, but they generally do not break data binding at all as long as you're aware of how the views are being used.

In the example above, I think the big problem is that when you loop over an object in SwiftUI, you need it to return something every single time. This is sort of a consequence of the way that Swift's syntax around builders works. So wrapping it in something like a group or stack works, but it would also work to wrap it in a special kind of view that actually does have the power to contain any number of elements, and you do that with @ViewBuilder.

So you could make something like:

Code Block
struct ConditionalTwoControl: View {
@Binding var effect: TwoControlEffect
@ViewBuilder var body: some View {
if effect.isDisplayed {
TwoControlTemplate(title: "Low Pass Filter",
isBypassed: $effect.isBypassed,
knobModel1: $effect.control1,
knobModel2: $effect.control2)
}
}
}


and then have that ForEach look like:

Code Block
ForEach(noise.twoControlEffects.indices){ i in
ConditionalTwoControl(effect: self.$noise.twoControlEffects[i])
}


And this would also work (with a little situational tweaking, of course).

More at hand to the situation here, I would consider trying to avoid looping over just an index if you can find a way around that, especially if you can just make your object conform to Identifiable. That's going to give you much more straightforward results with much more trace-able data binding.
I'm having similar trouble, but unfortunately iterating on the indices and passing an array item with subscript won't work for me. My ForEach statement iterates on the struct's name field and simultaneously sorts the array:

                    ForEach(store.cocktails.sorted {$0.cocktailName < $1.cocktailName }) { ******** in
                        CocktailCell(********: $********)
                    }

I don't want to give up the automatic sort, hence, I cannot access the index each time through in order to send a single array item. I do, however, have to pass a binding for the item because at each level below I either want to read the values in the struct or write to them.

The object "store" is instantiated with @StateObject at the parent level, and passed in as an ObservedObject at this view level.
Any ideas?
Jeez, that didn't post too well...replace the ******* in each case with the word c*ocktail. Ridiculous level of "disallow listing" words.
Running into a similar problem, if a value changes the entire text for the particular key value pair disappears:

Code Block
ForEach(viewCounts.sorted(by: <), id: \.key) { key, value in
Text("\(key) \(value)")
}

arithMattic You're a life saver! It's weird.

"Apparently, you just have to wrap the inside of your ForEach code in a HStack or VStack."

weird way to fix it all but hey it gets the job done!
I was having a similar issue when trying to create NavigationLinks inside of a ForEach loop. The destination View had random data from the iterative object (lesson in my case).

Code Block
           ForEach(viewModel.additionalLessons) { lesson in
                    NavigationLink(
                        destination: LessonView(viewModel: LessonViewModel(lesson: lesson,
                        isSelected: $viewModel.lessonSelected)),
                        isActive: $viewModel.lessonSelected,
                        label: {
                            LessonRow(lesson: lesson)
                        })
           }


I found both embedding it into a VStack AND removing the "isActive" parameter on the NavigationLink allowed me to Bind the iterative object (lesson) into my destination view. Now I consistently get an appropriate detail view on the row I select.

Code Block
      LazyVStack(spacing:0) {
          ForEach(viewModel.bookLessons) { lesson in
                    NavigationLink(
                        destination:
LessonView(viewModel: LessonViewModel(lesson: lesson,
isSelected: $viewModel.lessonSelected)),
                        label: {
                            LessonRow(lesson: lesson)
                        })
               }
}


The ForEach View cannot not work with indicies for dynamic data, it requires identifiers which is why we supply it with an array of Identifiable data.

This is so that it can calculate inserts, moves and deletions which obviously is impossible with an array of indicies which will in the case of moving 5 items around the indices will still be 0-4.

I had the same problem: A non-updating list. The problem was: The identifiable id (representing a database ID) of each element wasn't changing, but other content of the element. So I just added a SwiftUI id to the element enclosing View, like so:

ForEach(elements) {element in
  Button {
    ... actions ...
  } label: {
    MyView(content: element)
    .id("\(element.id)\(element.changingStuff.description)")
  } 
}

That solved the problem.

  • Thank you very much for this workaround! What is the intended implementation for something like a selection highlight? Seems to me we can only tuck on an .id or maybe custom modifier or track the selection state in the data elements them self, but that seems not so clean to me.

  • This worked for me as well. I have external state as ObservedObject and its value use as the ids of my list elements. When I update the external state, the entire list gets updated correctly.

Add a Comment