SwiftUI 中的@Binding 和 ForEach

@Binding and ForEach in SwiftUI

我不明白如何在 SwiftUI 中结合使用 @BindingForEach。假设我想从布尔数组创建一个 Toggle 列表。

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

我不知道如何将绑定到数组中的布尔值传递给每个 Toggle。上面的代码给出了这个错误:

Use of unresolved identifier '$boolVal'

好吧,这对我来说很好(当然)。我试过了:

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

这次错误是:

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

有办法解决这个问题吗?

您可以使用类似下面的代码。请注意,您会收到已弃用的警告,但要解决该问题,请查看其他答案:

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

在 SwiftUI 中,只需使用 Identifiable 结构而不是 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
}

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

旧版本,允许更改 Toggle 的数量(不仅是它们的值)。

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

⛔️ 不要使用错误的做法!

Most 的答案(包括@kontiki 接受的答案)方法导致引擎在每次更改时重新呈现整个 UI,Apple 在 wwdc2021(大约时间 7:40)


✅ Swift 5.5

从这个版本的 swift 开始,您可以通过传入可绑定项来直接使用绑定数组元素,例如:

⚠️ 注意 Swift 5.5 在 iOS 14 及以下不支持,但至少检查 os版本,不要继续这种不良做法!

Apple 在 WWDC21 视频中明确指出,在 ForEach 循环中使用 .indices 是一种不好的做法。除此之外,我们需要一种方法来唯一标识数组中的每一项,所以不能使用ForEach(boolArr, id:\.self),因为数组中有重复的值。

正如@Mojtaba Hosseini 所说,Swift 5.5 的新功能,您现在可以使用绑定数组元素直接传递可绑定项。但是如果你还需要使用之前版本的Swift,我是这样实现的:

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: [=10=].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: { [=10=].id == item.id }) else { return nil }
    return .init(get: { self.boolArr[index] },
                 set: { self.boolArr[index] = [=10=] })
  }
}

首先,我们通过创建一个符合 Identifiable 的简单结构,使数组中的每个项目都可识别。然后我们创建一个函数来创建自定义绑定。我本可以使用强制展开来避免从 makeBoolItemBinding 函数返回可选值,但我总是尽量避免它。从函数返回一个可选绑定需要 map 方法来解包它。

我已经在我的项目中测试过这个方法,到目前为止它工作得很好。