61

I can't undertand how to use @Binding in combination with ForEach in SwiftUI. Let's say I want to create a list of Toggles from an array of booleans.

struct ContentView: View {
    @State private var boolArr = [false, false, true, true, false]

    var body: some View {
        List {
            ForEach(boolArr, id: \.self) { boolVal in
                Toggle(isOn: $boolVal) {
                    Text("Is \(boolVal ? "On":"Off")")
                }                
            }
        }
    }
}

I don't know how to pass a binding to the bools inside the array to each Toggle. The code here above gives this error:

Use of unresolved identifier '$boolVal'

And ok, this is fine to me (of course). I tried:

struct ContentView: View {
    @State private var boolArr = [false, false, true, true, false]

    var body: some View {
        List {
            ForEach($boolArr, id: \.self) { boolVal in
                Toggle(isOn: boolVal) {
                    Text("Is \(boolVal ? "On":"Off")")
                }                
            }
        }
    }
} 

This time the error is:

Referencing initializer 'init(_:id:content:)' on 'ForEach' requires that 'Binding' conform to 'Hashable'

Is there a way to solve this issue?

Paul B
  • 3,989
  • 33
  • 46
superpuccio
  • 11,674
  • 8
  • 65
  • 93

5 Answers5

62

⛔️ Don't use a Bad practice!

Most of the answers (including the @kontiki accepted answer) method cause the engine to rerender the entire UI on each change and Apple mentioned this as a bad practice at wwdc2021 (around time 7:40)


✅ Swift 5.5

From this version of Swift, you can use binding array elements directly by passing in the bindable item like:

struct Model: Identifiable {
    var id: Int
    var text = ""
}

struct ContentView: View {
    @State var models = (0...9).map { Model(id: $0) }

    var body: some View {
        List($models) { $model in TextField("Instruction", text: $model.text) }
    }
}

⚠️ Note the usage of the $ syntax for all $models.

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • I understand but at least check for OS and don't continue the bad practice @paulz – Mojtaba Hosseini Jun 17 '21 at 06:30
  • 6
    Actually, this works well when targeting iOS 14.0 and on; just tested with Xcode 13 beta 3. Not all Swift 5.5 features require iOS 15. – Xtian D. Jul 16 '21 at 13:11
  • How do you use .onDelete in this case? – Lord Zsolt Oct 09 '21 at 13:41
  • 2
    I have not been able to get this to work. I get ```Cannot declare entity named '$direction'; the '$' prefix is reserved for implicitly-synthesized declarations``` and ```Initializer 'init(_:rowContent:)' requires that 'Binding<[String]>' conform to 'RandomAccessCollection'``` from $directions – Snipe3000 Nov 10 '21 at 09:40
  • While this answer might contain some useful info, it's very different from the OP's stated goal of "create a list of Toggles from an array of booleans". – Graham Lea Jul 21 '23 at 02:38
  • And then how do I make the binding List searchable? – Gianni Sep 02 '23 at 14:23
40

You can use something like the code below. Note that you will get a deprecated warning, but to address that, check this other answer: https://stackoverflow.com/a/57333200/7786555

import SwiftUI

struct ContentView: View {
    @State private var boolArr = [false, false, true, true, false]

    var body: some View {
        List {
            ForEach(boolArr.indices) { idx in
                Toggle(isOn: self.$boolArr[idx]) {
                    Text("boolVar = \(self.boolArr[idx] ? "ON":"OFF")")
                }
            }
        }
    }
}
kontiki
  • 37,663
  • 13
  • 111
  • 125
  • I would have accepted your answer, I really like the simplicity, but I decided to accept the warning free one. I'm curious to see what Apple we'll do about the subscript(_:) issue. Very likely we'll be able to improve the answers to this question in the future beta releases. – superpuccio Aug 05 '19 at 09:23
  • That's alright. Let's see if next week we have some progress on the matter. – kontiki Aug 05 '19 at 13:32
  • 1
    @superpuccio As mentioned in the linked answer, the beta 6 of Xcode fixed the warning. – Matteo Manferdini Dec 12 '19 at 12:04
  • @kontiki wasnt there something about the index version can only be used if the array's content stays the same? – Just a coder May 07 '20 at 21:14
  • 12
    This change to using indices also changes ForEach from displaying dynamic content to static content. If you want to remove an element from the list being iterated over, you may get an error. Docs: https://developer.apple.com/documentation/swiftui/foreach/3364099-init – Emma K Alexandra May 25 '20 at 16:54
  • 7
    To expand on what @EmmaKAlexandra is saying - your app will crash with an index out of bounds error if you are doing deletions. – Austin Aug 10 '20 at 02:40
  • @Austin Please provide solution for the case you mentioned for me. Thank you so much! – Duc Quang Dec 07 '20 at 06:28
  • 6
    Don't use indices. Bad code. There are red flags all over Swift and SwiftUI docs about this. Indices create bugs. Swift and SwiftUI provide MULTIPLE other means for getting array members. Who gave this a green checkmark?! – johnrubythecat Apr 15 '21 at 20:13
16

Update for Swift 5.5

struct ContentView: View {
    struct BoolItem: Identifiable {
      let id = UUID()
      var value: Bool = false
    }
    @State private var boolArr = [BoolItem(), BoolItem(), BoolItem(value: true), BoolItem(value: true), BoolItem()]

    var body: some View {
        NavigationView {
            VStack {
            List($boolArr) { $bi in
                Toggle(isOn: $bi.value) {
                        Text(bi.id.description.prefix(5))
                            .badge(bi.value ? "ON":"OFF")
                }
            }
                Text(boolArr.map(\.value).description)
            }
            .navigationBarItems(leading:
                                    Button(action: { self.boolArr.append(BoolItem(value: .random())) })
                { Text("Add") }
                , trailing:
                Button(action: { self.boolArr.removeAll() })
                { Text("Remove All") })
        }
    }
}

Previous version, which allowed to change the number of Toggles (not only their values).

struct ContentView: View {
   @State var boolArr = [false, false, true, true, false]
    
    var body: some View {
        NavigationView {
            // id: \.self is obligatory if you need to insert
            List(boolArr.indices, id: \.self) { idx in
                    Toggle(isOn: self.$boolArr[idx]) {
                        Text(self.boolArr[idx] ? "ON":"OFF")
                }
            }
            .navigationBarItems(leading:
                Button(action: { self.boolArr.append(true) })
                { Text("Add") }
                , trailing:
                Button(action: { self.boolArr.removeAll() })
                { Text("Remove All") })
        }
    }
}
Paul B
  • 3,989
  • 33
  • 46
  • 2
    Can we have something like that but with ForEach and not a list ? – GrandSteph Jun 12 '20 at 17:10
  • 2
    Of course we can, @GrandSteph, why not? `VStack { ForEach(boolArr.indices, id: \.self) { idx in Toggle(isOn: self.$boolArr[idx]) { Text(self.boolArr[idx] ? "ON":"OFF") } } }.padding()`. Or `HStack`, or` Group`, `Section`, etc. `List` just has this concise init (with `ForEach` included) since it is a very common thing for it ... to list items. – Paul B Jun 14 '20 at 09:30
  • 1
    Appreciated @Paul but I'm struggling with ForEach and dynamic list of array. I can't seem to satisfy the following 3 conditions : 1 - Dynamic array (indices is meant to be static, removing a element will crash) 2 - Use bindings so I can modify each element in subview 3 - Not use a list (I want custom design, no title etc ...) – GrandSteph Jun 14 '20 at 14:01
  • 2
    Crash on removal is an issue some people run into when deal with SwiftUI. In some cases using projected value can help: `func delete(at offsets: IndexSet) { $store.wrappedValue.data.remove(atOffsets: offsets) // instead of store.data.remove() }`. In other cases creating a var using `Binding()` init (not @Binding directive) is the best solution for this kind of issue. But the question is too general, @GrandSteph. Chances are you'll get a better answer if formulate it properly here at SO. Also you can check some variants of array driven interfaces here: https://stackoverflow.com/a/59739983/ – Paul B Jun 14 '20 at 19:28
  • And then how do I make the binding List searchable? – Gianni Sep 02 '23 at 14:23
8

In SwiftUI, just use Identifiable structs instead of Bools

struct ContentView: View {
    @State private var boolArr = [BoolSelect(isSelected: true), BoolSelect(isSelected: false), BoolSelect(isSelected: true)]

    var body: some View {
        List {
            ForEach(boolArr.indices) { index in
                Toggle(isOn: self.$boolArr[index].isSelected) {
                    Text(self.boolArr[index].isSelected ? "ON":"OFF")
                }
            }
        }
    }
}

struct BoolSelect: Identifiable {
    var id = UUID()
    var isSelected: Bool
}
Paul B
  • 3,989
  • 33
  • 46
Andre Carrera
  • 2,606
  • 2
  • 12
  • 15
  • Thanks for your answer. I don't actually really like the creation of a struct to wrap a simple bool value (since I could have used \.self to identify the bool itself), but your answer is warning free, so probably is the right answer at the moment (let's keep an eye on what Apple we'll do with the subscript(_:) issue) ps: the "Hashable" is actually redundant in the BoolSelect definition. – superpuccio Aug 05 '19 at 09:19
  • 1
    This seems to be broken now. The code above gives "Type of expression is ambiguous without context" in XCode 11 beta 2. – Tom Millard Oct 15 '19 at 09:53
  • And yet it seems to me that the latest Xcode can not handle the likes of "ForEach($boolArr)". I also get the "`type of expression is ambiguous without more context`"-error. When using a custom View in the ForEach loop and passing the loop variable as a `@Binding` the compiler finds yet another problem "Cannot invoke initializer for type 'ForEach<_, _, _>' with an argument list of type '`(Binding<[MyIdentifiable]>, @escaping (MyIdentifiable) -> MyCustomView)`'" – Enie Oct 15 '19 at 21:14
  • 1
    Thank you! I have been trying to get something like this to work for hours. The key for me was to use "indices" as the array to loop through. – Dan Nov 08 '19 at 02:25
  • 2
    Won't work if we have an array of `Identifiable` inside a `@State` struct and need to iterate over it. Will have to to use `List(model.boolArr.indices, id: \.self)` anyway if we plan to insert or remove array elements. – Paul B Jan 14 '20 at 20:41
  • This is the only thing that works for me. Ive tried with observable objects manually sending updates and even accessing the array directly like procedural swift. But using .indices was the only thing that worked after stumping me for days. Surely SwiftUI will adapt to iterate over a @State array without loosing the reference to the UI?! – Tofu Warrior Jul 28 '20 at 02:32
4

In WWDC21 videos Apple clearly stated that using .indices in the ForEach loop is a bad practice. Besides that, we need a way to uniquely identify every item in the array, so you can't use ForEach(boolArr, id:\.self) because there are repeated values in the array.

As @Mojtaba Hosseini stated, new to Swift 5.5 you can now use binding array elements directly passing the bindable item. But if you still need to use a previous version of Swift, this is how I accomplished it:

struct ContentView: View {
  @State private var boolArr: [BoolItem] = [.init(false), .init(false), .init(true), .init(true), .init(false)]
  
  var body: some View {
    List {
      ForEach(boolArr) { boolItem in
        makeBoolItemBinding(boolItem).map {
          Toggle(isOn: $0.value) {
            Text("Is \(boolItem.value ? "On":"Off")")
          }
        }
      }
    }
  }
  
  struct BoolItem: Identifiable {
    let id = UUID()
    var value: Bool
    
    init(_ value: Bool) {
      self.value = value
    }
  }
  
  func makeBoolItemBinding(_ item: BoolItem) -> Binding<BoolItem>? {
    guard let index = boolArr.firstIndex(where: { $0.id == item.id }) else { return nil }
    return .init(get: { self.boolArr[index] },
                 set: { self.boolArr[index] = $0 })
  }
}

First we make every item in the array identifiable by creating a simple struct conforming to Identifiable. Then we make a function to create a custom binding. I could have used force unwrapping to avoid returning an optional from the makeBoolItemBinding function but I always try to avoid it. Returning an optional binding from the function requires the map method to unwrap it.

I have tested this method in my projects and it works faultlessly so far.

vicentube
  • 81
  • 6