Views do not update inside the ForEach in SwiftUI

I'm using a

ForEach
to parse a list of models and create a view for each of them, Each view contains a
Button
and a
Text
, the
Button
toggles a visibility state which should hide the text and change the Button's title (Invisible/Visible).
struct ContentView: View {
    
    @State var colors: [MyColor] = [MyColor(val: "Blue"), MyColor(val: "Yellow"), MyColor(val: "Red")]
    
    var body: some View {
        
        ForEach(colors, id: \.uuid) { color in
            ButtonColorView(color: color.val)
        }
        
    }
}

struct ButtonColorView: View {
    
    var color: String
    
    @State var visible = true
    
    var body: some View {
        
        if visible {
            return AnyView(  HStack {
                Button("Invisible") {
                    self.visible.toggle()
                }
                Text(color)
            })
        } else {
            return AnyView(
                Button("Visible") {
                    self.visible.toggle()
                }
            )
        }
    }
}



class MyColor: Identifiable {
    let uuid = UUID()
    let val: String
    
    init(val: String) {
        self.val = val
    }
}

Unfortunately it's not working, the views inside the

ForEach
do not change when the
Button
is pressed. I replaced the
Foreach
with
ButtonColorView(color: colors[0].val)
and it seems to work, so I'd say the problem is at
ForEach
.

I also tried breakpoints in

ButtonColorView
and it seems the view is called when the Button is triggered returning the right view, anyways the view does not update on screen.

So, am I using the

ForEach
in a wrong way ?

This problem occurs in a more complex app, but I tried to extract it in this small example.


To summarize it: I need

ButtonColorView
to return different Views depending of its state (visibility in this case)



PS: I'm using Xcode 11 Beta 6

Replies

I am having a similar problem with Picker:


class Category: Identifiable {
    let uuid = UUID()
    let label: String
    init (_ label: String) {
        self.label = label
    }
}

class SessionModel: ObservableObject {
    let didChange = PassthroughSubject<Void, Never>()
    
    @Published var categories: [Category] = [Category("Home"), Category("Work"), Category("Financial"), Category("Entertainmen")]
    @Published var categoryIndex = 0
    
    func selectedCategory() -> String {
        categories[categoryIndex].label
    }
    
    func addCategory(_ newCategory: String) {
        categories.append(Category(newCategory))
        didChange.send()
    }
}
// usage
               Picker(
                    selection: self.$viewModel.categoryIndex,
                    label: Text("Colors")
                ) {
                    ForEach(self.viewModel.categories) { category in
                        Text(category.label)
                    }
                }
                .pickerStyle(WheelPickerStyle())


If you change the model (self.viewModel.addCategory("New Item")) the ForEach will not trigger. I don't know what else to do.

I have the same problem with my array of objects. 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). What does the ForEach loop do that breaks databinding?

// Does data bind correctly
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 data bind correctly
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 data bind correctly
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)
     }
}


UPDATE - I found a simple solution that worked for me. It may work for you too.

See my post:

https://forums.developer.apple.com/thread/131577