For some reason I don't understand, when I add/remove items from a @State var
in MainView
, the OutterView
s are not being updated properly.
What I am trying to achieve is that the user can only "flag" (select) one item at a time. For instance, when I click on "item #1" it will be flagged. If I click on another item then "item #1" will not be flagged anymore but only the new item I just clicked.
Currently, my code shows all items as if they were flagged even when they are not anymore. The following code has the minimum structure and functionality I'm implementing for MainView
, OutterView
, and InnerView
.
I've tried using State var
s instead of the computed property in OutterView
, but it doesn't work. Also, I tried using a var
instead of the computed property in OutterView
and initialized it in init()
but also doesn't work.
Hope you can help me to find what I am doing wrong. Thanks!
struct MainView: View {
@State var flagged: [String] = []
var data: [String] = ["item #1", "item #2", "item #3", "item #4", "item #5"]
var body: some View {
VStack(spacing: 50) {
VStack {
ForEach(data, id:\.self) { text in
OutterView(text: text, flag: flagged.contains(text)) { (flag: Bool) in
if flag {
flagged = [text]
} else {
if let index = flagged.firstIndex(of: text) {
flagged.remove(at: index)
}
}
}
}
}
Text("Flagged: \(flagged.description)")
Button(action: {
flagged = []
}, label: {
Text("Reset flagged")
})
}
}
}
struct OutterView: View {
@State private var flag: Bool
private let text: String
private var color: Color { flag ? Color.green : Color.gray }
private var update: (Bool)->Void
var body: some View {
InnerView(color: color, text: text)
.onTapGesture {
flag.toggle()
update(flag)
}
}
init(text: String, flag: Bool = false, update: @escaping (Bool)->Void) {
self.text = text
self.update = update
_flag = State(initialValue: flag)
}
}
struct InnerView: View {
let color: Color
let text: String
var body: some View {
Text(text)
.padding()
.background(
Capsule()
.fill(color))
}
}
Here's a simple version that does what you're looking for (explained below):
What's happening:
Item
that has an ID for each item, the flagged state of that item, and the titleStateManager
keeps an array of those items. It also has a custom binding for each index of the array. For thegetter
, it just returns the state of the model at that index. For thesetter
, it makes a new copy of the item array. Any time a checkbox is set, it unchecks all of the other boxes.ForEach
now gets an enumeration of theitems
. This could be done without enumeration, but it was easy to write the custom binding by index like this. You could also filter by ID instead of index. Note that because of the enumeration, it's using.1.id
for the id parameter --.1
is the item while.0
is theindex
.ForEach
, the custom binding from before is created and passed to the subviewBinding
is passed to)Using this strategy of an
ObservableObject
that contains all of your state and passes it on via @Published properties and @Bindings makes organizing your data a lot easier. It also avoids having to pass closures back and forth like you were doing initially with yourupdate
function. This ends up being a pretty idiomatic way of doing things in SwiftUI.