SwiftUI Edit Struct from List

1.2k views Asked by At

I am attempting to have a list that when a cell it tapped it changes the hasBeenSeen Bool value within the State object itself.

struct State: Identifiable {
    var id = UUID()
    let name: String
    var hasBeenSeen: Bool = false
}

struct ContentView: View {
    let states: [State] = [
        State(name: "Oregon", hasBeenSeen: true),
        State(name: "California", hasBeenSeen: true),
        State(name: "Massachussets", hasBeenSeen: false),
        State(name: "Washington", hasBeenSeen: true),
        State(name: "Georgia", hasBeenSeen: false)
    ]
    
    var body: some View {
        NavigationView {
            List {
                ForEach(states, id: \.id) { state in
                    StateCell(state: state)
                }
            }.navigationBarTitle(Text("States"))
        }
    }
}

struct StateCell: View {
    var state: State
    
    var body: some View {
        HStack {
            Text(state.name)
            Spacer()
            if state.hasBeenSeen {
                Image(systemName: "eye.fill")
            }
        }.onTapGesture {
//            state.hasBeenSeen.toggle()
        }
    }
}

My original thought is that I need to make hasBeenSeen to a @State var but that doesn't seem to work. How can I make this Bool val editable from a list?

1

There are 1 answers

0
New Dev On

Views in SwiftUI are immutable - they are just structures - so you can't change their properties. That's why SwiftUI has a concept of a @State property wrapper. When you change "state" property, SwiftUI actually updates the state value, not the view's property value (which is, again, immutable).

So, you need to set @State on the states property within your view. (You'd also need to change the name, since identifier State is already taken by the State property wrapper - so I just changed it to StateEntity)

@State var states: [StateEntity] = [
      StateEntity(name: "Oregon", hasBeenSeen: true),
      // ... etc
   ]

That's not enough, though, since when you pass an element of the states array (a StateEntity value) to a child view, you're just passing a copy.

For that, you'd need a binding. A binding allows child views to modify state properties of parent views, without owning the data. So, the child view's property should use the @Binding property wrapper:

struct StateCell: View {
    @Binding var state: StateEntity

    // ...
}

SwiftUI made it easy to get the binding of state property by using the projected value, which in this case is $states.

However, you need to pass a binding not to the entire array, but to a specific element of that array. That, unfortunately (and rather annoyingly), is a bit trickier. You need to get the index of the element, and given the index, access the binding like so: $state[index].

One way is to do a ForEach over indices of states:

var body: some View {
   NavigationView {
      List {
         ForEach(states.indices) { index in
            StateCell(state: self.$states[index])
         }
      }.navigationBarTitle(Text("States"))
   }
}