40
votes

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?

5

5 Answers

38
votes

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")")
                }
            }
        }
    }
}
9
votes

⛔️ 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:

enter image description here

⚠️ Note that Swift 5.5 is not supported on iOS 14 and below but at least check for the os version and don't continue the bad practice!

8
votes

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
}
8
votes

If also you need to change the number of Toggles (not only their values), consider this. BTW, we can omit ForEach {} block.

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") })
        }
    }
}
2
votes

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.