SwiftUI 中的@Binding 和 ForEach
@Binding and ForEach in SwiftUI
我不明白如何在 SwiftUI 中结合使用 @Binding
和 ForEach
。假设我想从布尔数组创建一个 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 方法来解包它。
我已经在我的项目中测试过这个方法,到目前为止它工作得很好。
我不明白如何在 SwiftUI 中结合使用 @Binding
和 ForEach
。假设我想从布尔数组创建一个 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 方法来解包它。
我已经在我的项目中测试过这个方法,到目前为止它工作得很好。