74

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 Answers 5

82

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

Sign up to request clarification or add additional context in comments.

7 Comments

I understand but at least check for OS and don't continue the bad practice @paulz
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.
How do you use .onDelete in this case?
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
This should be the accepted answer
|
41

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

8 Comments

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 As mentioned in the linked answer, the beta 6 of Xcode fixed the warning.
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: developer.apple.com/documentation/swiftui/foreach/3364099-init
To expand on what @EmmaKAlexandra is saying - your app will crash with an index out of bounds error if you are doing deletions.
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?!
|
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") })
        }
    }
}

5 Comments

Can we have something like that but with ForEach and not a list ?
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.
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 ...)
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: stackoverflow.com/a/59739983
And then how do I make the binding List searchable?
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
}

6 Comments

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.
This seems to be broken now. The code above gives "Type of expression is ambiguous without context" in XCode 11 beta 2.
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)'"
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.
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.
|
5

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.