@Binding和ForEach在SwiftUI中的应用

63

我不明白如何在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。 上面的代码会导致以下错误:

未解决的标识符“ $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")")
                }                
            }
        }
    }
} 

这次的错误是:

对 'ForEach' 上的初始化器 'init(_:id:content:)' 的引用要求 'Binding' 遵循 'Hashable'

有办法解决这个问题吗?

5个回答

66
⛔️ 不要使用不良实践!
大多数答案(包括@kontiki接受的答案)的方法会导致引擎在每次更改时重新渲染整个用户界面,苹果在wwdc2021(大约在7:40左右)提到了这种做法是一种不良实践。

✅ Swift 5.5

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

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

⚠️请注意,所有的$model都使用了$语法。

我理解,但至少要检查操作系统并避免继续不良实践 @paulz - Mojtaba Hosseini
6
实际上,在针对 iOS 14.0 及以上版本时,这个功能运行良好;我刚在 Xcode 13 beta 3 中进行了测试。并非所有 Swift 5.5 功能都需要 iOS 15。 - Xtian D.
在这种情况下,你如何使用 .onDelete? - Lord Zsolt
2
我无法让它正常工作。我收到了Cannot declare entity named '$direction'; the '$' prefix is reserved for implicitly-synthesized declarationsInitializer 'init(_:rowContent:)' requires that 'Binding<[String]>' conform to 'RandomAccessCollection'来自$directions。 - Snipe3000
虽然这个回答可能包含一些有用的信息,但它与原帖中所述的目标“从布尔数组创建一个开关列表”非常不同。 - Graham Lea
然后,我该如何使绑定的列表可搜索? - Gianni

40

您可以使用以下代码。请注意,您将收到一个已弃用的警告,但为了解决这个问题,请查看这个其他答案:https://dev59.com/iVMI5IYBdhLWcg3wA2js#57333200

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

1
@superpuccio 如链接答案中所述,Xcode的beta 6修复了该警告。 - Matteo Manferdini
@kontiki 有关于索引版本只能在数组内容不变的情况下使用的问题,你还记得吗? - Just a coder
12
将索引用于 ForEach 后,它将显示静态而非动态内容。如果您想从正在遍历的列表中删除元素,则可能会出现错误。文档链接:https://developer.apple.com/documentation/swiftui/foreach/3364099-init - Emma K Alexandra
7
扩展@EmmaKAlexandra所说的内容 - 如果进行删除操作,您的应用程序可能会因索引越界错误而崩溃。 - Austin
6
不要使用索引。代码不好。Swift 和 SwiftUI 的文档中都有关于此的警告标志。索引会产生 bug。Swift 和 SwiftUI 提供多种其他方法来获取数组成员。是谁给了这个绿色勾号?! - johnrubythecat
显示剩余3条评论

16

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

之前的版本可以更改 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") })
        }
    }
}

2
我们能否使用ForEach而不是列表来实现类似的功能? - GrandSteph
2
当然可以,@GrandSteph,为什么不呢? VStack { ForEach(boolArr.indices, id: \.self) { idx in Toggle(isOn: self.$boolArr[idx]) { Text(self.boolArr[idx] ? "ON":"OFF") } } }.padding(). 或者使用 HStackGroupSection 等等。List 只有这个简洁的初始化(包括 ForEach)是因为它是一个非常常见的东西...用于列出项目。 - Paul B
1
感谢@Paul的帮助,但我在ForEach和动态数组列表方面遇到了困难。我似乎无法满足以下三个条件:1-动态数组(索引应为静态,删除元素将导致崩溃)2-使用绑定,以便我可以修改子视图中的每个元素3-不使用列表(我想要自定义设计,没有标题等...) - GrandSteph
2
删除导致崩溃是一些人在处理SwiftUI时遇到的问题。在某些情况下,使用投影值可以帮助解决:func delete(at offsets: IndexSet) { $store.wrappedValue.data.remove(atOffsets: offsets) // instead of store.data.remove() }。在其他情况下,使用Binding()初始化程序创建变量(而不是@Binding指令)是这种问题的最佳解决方案。但问题太笼统了,@GrandSteph。如果您在SO上适当地制定它,那么您很可能会得到更好的答案。另外,您可以在此处检查一些基于数组的接口变体:https://dev59.com/oLTma4cB1Zd3GeqP5V-g#59739983/ - Paul B
然后,我该如何使绑定列表可搜索? - Gianni

8
在SwiftUI中,只需使用可识别的结构体,而不是布尔型。
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
}

感谢您的回答。实际上,我并不喜欢创建一个结构体来包装一个简单的布尔值(因为我本可以使用 .self 来标识布尔本身),但是您的答案没有警告,所以现在可能是正确答案(让我们关注一下苹果对 subscript(_:) 问题的处理)。另外,“Hashable” 在 BoolSelect 定义中实际上是多余的。 - superpuccio
1
现在似乎出了问题。在XCode 11 beta 2中,上面的代码会显示“表达式类型不明确”。 - Tom Millard
然而,我认为最新的Xcode无法处理"ForEach($boolArr)"这样的内容。我还遇到了"type of expression is ambiguous without more context"错误。当在ForEach循环中使用自定义视图并将循环变量作为@Binding传递时,编译器会发现另一个问题:"Cannot invoke initializer for type 'ForEach<_, _, _>' with an argument list of type '(Binding<[MyIdentifiable]>, @escaping (MyIdentifiable) -> MyCustomView)'"。 - Enie
1
谢谢!我已经尝试了几个小时才让这个东西起作用。对我来说关键是使用“indices”作为要循环遍历的数组。 - Dan
2
如果我们在一个@State结构体内有一个Identifiable数组并需要对其进行迭代,那么它将无法工作。如果我们计划插入或删除数组元素,仍然必须使用List(model.boolArr.indices, id: \.self) - Paul B
这是我唯一有效的方法。我尝试过使用可观察对象手动发送更新,甚至像过程式 Swift 一样直接访问数组。但是在困扰了我数天后,使用 .indices 是唯一有效的方法。毫无疑问,SwiftUI 将适应迭代 @State 数组而不会失去对 UI 的引用! - Tofu Warrior

4
在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: $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 })
  }
}

首先,我们通过创建一个简单的符合 Identifiable 的结构体来使数组中的每个项目都可以标识。然后,我们创建一个自定义绑定函数。我本可以使用强制解包来避免从 makeBoolItemBinding 函数返回可选项,但我总是尽量避免这样做。从函数返回可选绑定需要使用 map 方法进行解包。
我已经在我的项目中测试了这种方法,到目前为止它运行得非常顺畅。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接