DEV Community

Diego Lavalle for Swift You and I

Posted on • Originally published at swiftui.diegolavalle.com

On-demand bindings

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.

On-demand bindings

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.

macOS version of the example

Check out the associated Working Example to see this technique in action.

FEATURED EXAMPLE: Toggles - Add'em, flip'em, hide'em!

Top comments (0)