Bindings are used to pass parts of our state down to a subview that might later on make changes to said state. We typically rely on the @State
and @Binding
property wrappers to generate bindings for us but bindings can also be constructed programmatically. In fact there’s a handful of cases in which creating bindings in an on-demand manner may just be the preferred option.
Like most UI controls in SwiftUI, Toggle is backed by a binding. In this case the initializer argument isOn of type Binding<Bool>
informs the toggle whether it is on or off but most importantly, it also allows the control to alter the value of this variable from the inside. Now imagine that we have -as part of our state- a whole set of toggable models which we want represented in our user interface as individual toggle controls.
struct Toggles: View {
struct Model: Identifiable {
var id: String // Also used as label
var on = false
}
@State var items = [
Model(id: "foo"),
Model(id: "bar"),
Model(id: "baz"),
]
var body: some View { … }
}
We can leverage SwiftUI’s auto-generated bindings and reference the $items
property which is bound to the items
state variable. Given that ForEach can only iterate over sequences -not bindings of sequences- we need initialize it with items.indices
instead. Each index gives us direct access to both an element of our state -a Model
instance- and its corresponding binding.
var body: some View {
VStack {
ForEach(items.indices) {
Toggle(
self.items[$0].id, // Label
isOn: self.$items[$0].on // Binding
)
} // ForEach
} // VStack
} // body
This works fine except for one thing: the specification of ForEach
warns us that iterating over ranges only applies to constant data. Now as far as our view is concerned, the element count for items
could change overtime since the whole array is tagged with @State
on its declaration. Let’s suppose we want to enable the possibility of removing a toggle altogether.
ForEach(…) {
HStack {
Toggle(…)
Button("remove") {
// TODO: remove item from items array
}
}
}
Because we made our Model
struct comply with the Identifiable
protocol it’s easy iterate over our items. But we still need a binding for Toggle to work with. We can instantiate the binding on-the-fly provided that we always use up-to-date indices to look up for items inside our array.
…
func makeBinding(_ item: Model) -> Binding<Bool> {
let i = self.items.firstIndex { $0.id == item.id }!
return .init(
get: { self.available[i].on },
set: { self.available[i].on = $0 }
)
}
var body: some View {
…
ForEach(items) { item in
HStack {
Toggle(
item.id,
isOn: self.makeBinding(item)
)
Button("remove") {
self.items.removeAll { $0.id == item.id }
}
}
}
…
}
We call the makeBinding
function every time a toggle is rendered. The function first finds the current index for the specified item and uses it to produce both the getter and setter that make up the binding object. The remove button simply mutates our items array normally since it’s declared as part of the view’s state.
Check out the associated Working Example to see this technique in action.