6

How do I build a dynamic list with @Binding-driven controls without having to reference the array manually? It seems obvious but using List or ForEach to iterate through the array give all sorts of strange errors.

struct OrderItem : Identifiable {
    let id = UUID()
    var label : String
    var value : Bool = false
}

struct ContentView: View {
    @State var items = [OrderItem(label: "Shirts"),
                        OrderItem(label: "Pants"),
                        OrderItem(label: "Socks")]
    var body: some View {
        NavigationView {
            Form {
                Section {
                    List {
                        Toggle(items[0].label, isOn: $items[0].value)
                        Toggle(items[1].label, isOn: $items[1].value)
                        Toggle(items[2].label, isOn: $items[2].value)
                    }
                }
            }.navigationBarTitle("Clothing")
        }
    }
}

This doesn't work:

            ...
                Section {
                    List($items, id: \.id) { item in
                        Toggle(item.label, isOn: item.value)
                    }
                }
            ...

Type '_' has no member 'id'

Nor does:

            ...
                Section {
                    List($items) { item in
                        Toggle(item.label, isOn: item.value)
                    }
                }
            ...

Generic parameter 'SelectionValue' could not be inferred

joshd
  • 1,626
  • 14
  • 17
  • Solving this would answer the bigger objective: To create a ToggleList View that can take $items and manage, display the toggles. – joshd Sep 07 '19 at 19:51

2 Answers2

11

Try something like

...
   Section {
       List(items.indices) { index in
           Toggle(self.items[index].label, isOn: self.$items[index].value)
       }
   }
...
Olcay Ertaş
  • 5,987
  • 8
  • 76
  • 112
Maki
  • 469
  • 3
  • 6
  • Wow. Can't believe I missed that. I was hoping I'd be able to pass an actual object but that gets me where I needed to go. Thanks! – joshd Sep 08 '19 at 06:26
  • 2
    I can't believe it!! It does work. Why would this work and passing the object doesn't??!! – user1366265 Sep 23 '19 at 18:31
  • 3
    This approach seems to break when the amount of items in the array gets reduced later. See https://stackoverflow.com/questions/57631225/using-foreach-loop-with-binding-causes-index-out-of-range-when-array-shrinks-sw – Tom Millard Oct 14 '19 at 09:35
8

While Maki's answer works (in some cases). It is not optimal and it's discouraged by Apple. Instead, they proposed the following solution during WWDC 2021:

Simply pass a binding to your collection into the list, using the normal dollar sign operator, and SwiftUI will pass back a binding to each individual element within the closure.

Like this:

struct ContentView: View {
    @State var items = [OrderItem(label: "Shirts"),
                        OrderItem(label: "Pants"),
                        OrderItem(label: "Socks")]

    var body: some View {
        NavigationView {
            Form {
                Section {
                    List($items) { $item in
                        Toggle(item.label, isOn: $item.value)
                    }
                }
            }.navigationBarTitle("Clothing")
        }
    }
}
Damiaan Dufaux
  • 4,427
  • 1
  • 22
  • 33
  • Interesting. Is this limited to new APIs? – joshd Jun 09 '21 at 18:10
  • 1
    According to Matt Ricketson: "you can even back-deploy this code to any prior release supported by SwiftUI". In fact I think it uses a feature from [SE-0293](https://github.com/apple/swift-evolution/blob/main/proposals/0293-extend-property-wrappers-to-function-and-closure-parameters.md#closures) (which is implemented in Swift 5.5). But only installing the 5.5 toolchain in the old (v12) Xcode is not enough. I think you will also need to add the init(projectedValue:) to Binding and make it conform to the RandomAccessCollection in order to use this tweak with older versions of SwiftUI. – Damiaan Dufaux Jun 10 '21 at 09:16
  • I confirm this is the way to set an individual binding to each row dynamically. As a side note, l noticed that this doesn't work on Publisher wrappers. Only State wrappers. – zouritre Jan 16 '23 at 22:28