SwiftUI 中的 @Binding 和 ForEach

Posted

技术标签:

【中文标题】SwiftUI 中的 @Binding 和 ForEach【英文标题】:@Binding and ForEach in SwiftUI 【发布时间】:2019-12-11 21:55:40 【问题描述】:

我无法理解如何在 SwiftUI 中将 @BindingForEach 结合使用。假设我想从一个布尔数组中创建一个 Toggles 列表。

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'

有没有办法解决这个问题?

【问题讨论】:

【参考方案1】:

您可以使用类似下面的代码。请注意,您将收到一条已弃用的警告,但要解决此问题,请查看其他答案:https://***.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")")
                
            
        
    

【讨论】:

我会接受你的回答,我真的很喜欢简单,但我决定接受无警告的回答。我很想知道苹果将如何处理下标(_:) 问题。我们很可能能够在未来的测试版中改进这个问题的答案。 @superpuccio 如链接答案中所述,Xcode 的 beta 6 修复了警告。 对使用索引的更改也将 ForEach 从显示动态内容更改为静态内容。如果你想从被迭代的列表中删除一个元素,你可能会得到一个错误。文档:developer.apple.com/documentation/swiftui/foreach/3364099-init 扩展 @EmmaKAlexandra 所说的内容 - 如果您正在执行删除操作,您的应用将因索引越界错误而崩溃。 不要使用索引。错误的代码。 Swift 和 SwiftUI 文档中都有关于此的危险信号。索引会产生错误。 Swift 和 SwiftUI 提供了多种其他方式来获取数组成员。谁给了这个绿色复选标记?!【参考方案2】:

⛔️ 不要使用坏习惯!

大多数答案(包括@kontiki 接受的答案)方法会导致引擎在每次更改时重新呈现整个 UI,Apple 在 wwdc2021(大约 7:40)提到这是一种不好的做法


✅斯威夫特 5.5

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

⚠️ 注意 ios 14 及更低版本不支持 Swift 5.5 但至少检查操作系统版本,不要继续不良做法!

【讨论】:

我明白,但至少检查操作系统,不要继续这种不良做法@paulz 实际上,这适用于 iOS 14.0 及更高版本;刚刚使用 Xcode 13 beta 3 进行了测试。并非所有 Swift 5.5 功能都需要 iOS 15。 在这种情况下如何使用 .onDelete? 我无法让它工作。我从 $directions 得到 Cannot declare entity named '$direction'; the '$' prefix is reserved for implicitly-synthesized declarationsInitializer 'init(_:rowContent:)' requires that 'Binding<[String]>' conform to 'RandomAccessCollection'【参考方案3】:

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

以前的版本,允许更改Toggles 的数量(不仅仅是它们的值)。

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

【讨论】:

我们可以有类似的东西,但使用 ForEach 而不是列表? 当然可以,@GrandSteph,为什么不呢? VStack ForEach(boolArr.indices, id: \.self) idx in Toggle(isOn: self.$boolArr[idx]) Text(self.boolArr[idx] ? "ON":"OFF") .padding()。或者HStack,或者`Group, Section, etc. List`只有这个简洁的初始化(包括ForEach),因为它是一个非常常见的事情......列出项目。 感谢@Paul,但我正在努力处理 ForEach 和动态数组列表。我似乎无法满足以下 3 个条件:1 - 动态数组(索引是静态的,删除元素会崩溃) 2 - 使用绑定,以便我可以修改子视图中的每个元素 3 - 不使用列表(我想要定制设计,没有标题等...) 删除时崩溃是一些人在处理 SwiftUI 时遇到的问题。在某些情况下,使用预计值会有所帮助:func delete(at offsets: IndexSet) $store.wrappedValue.data.remove(atOffsets: offsets) // instead of store.data.remove() 。在其他情况下,使用 Binding() init(不是 @Binding 指令)创建 var 是此类问题的最佳解决方案。但是这个问题太笼统了,@GrandSteph。如果在 SO 正确地制定它,您可能会得到更好的答案。您还可以在此处查看数组驱动接口的一些变体:***.com/a/59739983【参考方案4】:

在 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

【讨论】:

感谢您的回答。我实际上并不喜欢创建一个结构来包装一个简单的 bool 值(因为我可以使用 \.self 来识别 bool 本身),但是您的答案是无警告的,所以目前可能是正确的答案(让我们密切关注 Apple 我们将如何处理下标(_:) 问题) ps:“Hashable”实际上在 BoolSelect 定义中是多余的。 现在好像坏了。上面的代码在 XCode 11 beta 2 中给出了“表达式类型在没有上下文的情况下不明确”。 但在我看来,最新的 Xcode 无法处理“ForEach($boolArr)”之类的问题。我还收到“type of expression is ambiguous without more context”错误。当在 ForEach 循环中使用自定义视图并将循环变量作为 @Binding 传递时,编译器发现另一个问题“无法使用类型为 '@987654324 的参数列表调用类型为 'ForEach<_ _>' 的初始化程序” @'" 谢谢!我一直试图让这样的东西工作几个小时。对我来说,关键是使用“索引”作为循环遍历的数组。 如果我们在@State 结构中有一个Identifiable 数组并且需要对其进行迭代,则将无法工作。如果我们打算插入或删除数组元素,无论如何都必须使用List(model.boolArr.indices, id: \.self)【参考方案5】:

在 WWDC21 视频中,Apple 明确指出在 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 方法来解包它。

我已经在我的项目中测试过这种方法,到目前为止它运行良好。

【讨论】:

以上是关于SwiftUI 中的 @Binding 和 ForEach的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI 中的 @Binding 和 ForEach

使用 Binding 确定 SwiftUI 中的字符串更改

从 @Binding var 修改 @State var 不会刷新 SwiftUI 中的视图

SwiftUI 将@Published viewmodel 对象值传递给@Binding

SwiftUI 中的可选绑定

SwiftUI 自定义 List 不能在 ForEach 中使用 Binding