【问题标题】:Deleting list elements from SwiftUI's List从 SwiftUI 的 List 中删除列表元素
【发布时间】:2020-07-24 18:08:27
【问题描述】:

SwiftUI 似乎有一个相当烦人的限制,这使得在获取每个元素的绑定以传递给子视图时很难创建 ListForEach

我见过的最常见的建议方法是迭代索引,并获得与$arr[index] 的绑定(事实上,当他们删除BindingCollection 的一致性时,类似的东西是suggested by Apple) :

@State var arr: [Bool] = [true, true, false]

var body: some View {
   List(arr.indices, id: \.self) { index in
      Toggle(isOn: self.$arr[index], label: { Text("\(idx)") } )
   }
}

这工作直到数组的大小发生变化,然后它因索引超出范围运行时错误而崩溃。

这是一个会崩溃的例子:

class ViewModel: ObservableObject {
   @Published var arr: [Bool] = [true, true, false]
    
   init() {
      DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
         self.arr = []
      }
   }
}

struct ContentView: View {
   @ObservedObject var vm: ViewModel = .init()

   var body: some View {
      List(vm.arr.indices, id: \.self) { idx in
         Toggle(isOn: self.$vm.arr[idx], label: { Text("\(idx)") } )
      }
  }
}

什么是处理从列表中删除的正确方法,同时仍保持使用绑定修改其元素的能力?

【问题讨论】:

  • Xcode 12 / iOS 14 不会崩溃
  • @Asperi - 很有趣。感谢您的发现。不确定这是 Apple 有意修复还是其他原因
  • 确实很有趣。如果您使用 List 它不会崩溃,但如果您将 List 替换为具有相同签名的 ForEach - 它会崩溃(xCode 12 Beta 5)

标签: swift swiftui


【解决方案1】:

利用来自@pawello2222 和@Asperi 的见解,我想出了一种我认为行之有效的方法,而且不会过于讨厌(仍然有点老套)。

我想让该方法更通用,而不仅仅是问题中的简化示例,也不是破坏关注点分离的方法。

所以,我创建了一个新的包装视图,它创建了一个与自身内部的数组元素的绑定(这似乎根据@pawello2222 的观察修复了状态失效/更新顺序),并将绑定作为参数传递给内容闭包.

我最初预计需要对索引进行安全检查,但事实证明这个问题不需要。

struct Safe<T: RandomAccessCollection & MutableCollection, C: View>: View {
   
   typealias BoundElement = Binding<T.Element>
   private let binding: BoundElement
   private let content: (BoundElement) -> C

   init(_ binding: Binding<T>, index: T.Index, @ViewBuilder content: @escaping (BoundElement) -> C) {
      self.content = content
      self.binding = .init(get: { binding.wrappedValue[index] }, 
                           set: { binding.wrappedValue[index] = $0 })
   }
   
   var body: some View { 
      content(binding)
   }
}

用法是:

@ObservedObject var vm: ViewModel = .init()

var body: some View {
   List(vm.arr.indices, id: \.self) { index in
      Safe(self.$vm.arr, index: index) { binding in
         Toggle("", isOn: binding)
         Divider()
         Text(binding.wrappedValue ? "on" : "off")
      }
   }
}

【讨论】:

  • 看起来不错。我认为您只需要将@ViewBuilder content 参数转义@escaping (BoundElement) -&gt; C。而您的Safe.init 需要为第一个参数添加一个“绑定”标签 - 在您的示例中没有 (Safe(self.$vm.arr, ...)。
  • 我不确定它是如何工作的或为什么需要它,但它帮助我解决了我的索引超出范围的问题。
  • 这很好用。正如在其他 cmets 中所述,我仅在使用 ForEach 时遇到此问题,并且在最新版本中似乎已为 List 解决。但是,在许多情况下,List 是该工作的错误组件。在 Apple 解决此问题之前将使用此解决方案。
【解决方案2】:

您的Toggle 似乎在List 之前刷新(可能是一个错误,已在 SwiftUI 2.0 中修复)。

您可以将您的行提取到另一个视图并检查索引是否仍然存在。

struct ContentView: View {
    @ObservedObject var vm: ViewModel = .init()

    var body: some View {
        List(vm.arr.indices, id: \.self) { index in
            ToggleView(vm: self.vm, index: index)
        }
    }
}

struct ToggleView: View {
    @ObservedObject var vm: ViewModel
    let index: Int
    
    @ViewBuilder
    var body: some View {
        if index < vm.arr.count {
            Toggle(isOn: $vm.arr[index], label: { Text("\(vm.arr[index].description)") })
        }
    }
}

这样ToggleView 将在List 之后刷新

如果你在 ContentView 内做同样的事情,它仍然会崩溃:

ContentView {
    ...
    @ViewBuilder
    func toggleView(forIndex index: Int) -> some View {
        if index < vm.arr.count {
            Toggle(isOn: $vm.arr[index], label: { Text("\(vm.arr[index].description)") })
        }
    }
}

【讨论】:

  • 太棒了!通过将@ViewBuilder 添加到ToggleView.body 并删除Group 使其更加优雅。
  • @Asperi 感谢您的建议,@ViewBuilder 肯定更好。
  • @pawello2222,感谢您的回复。确实,似乎某些事情正在被无序地评估。您的解决方案适用于本示例,但理想情况下,最好不要破坏关注点分离,因为理想情况下 ToggleView 不应该知道任何关于其父视图模型的信息。
  • @NewDev 没错,幸运的是它看起来在 SwiftUI 2.0 中已修复。
【解决方案3】:

SwiftUI 2.0

经 Xcode 12 / iOS 14 测试 - 崩溃不可重现

SwiftUI 1.0+

由于与已删除元素的悬挂绑定发生了崩溃(可能是“无效/更新顺序错误的原因”)。 这是一个安全的解决方法。使用 Xcode 11.4 / iOS 13.4 测试

struct ContentView: View {
    @ObservedObject var vm: ToggleViewModel = .init()

    var body: some View {
        List(vm.arr.indices, id: \.self, rowContent: row(for:))
    }

    // helper function to have possibility to generate & inject proxy binding
    private func row(for idx: Int) -> some View {
        let isOn = Binding(
            get: {
                // safe getter with bounds validation
                idx < self.vm.arr.count ? self.vm.arr[idx] : false
            },
            set: { self.vm.arr[idx] = $0 }
        )
        return Toggle(isOn: isOn, label: { Text("\(idx)") } )
    }
}

【讨论】:

  • 谢谢。该解决方案适用于 Swift 5.5、Xcode 12.5.1/iOS 14.6。
【解决方案4】:

如果有人感兴趣,我将 New dev 的 Safe solutionForEach 结合起来:

struct ForEachSafe<T: RandomAccessCollection & MutableCollection, C: View>: View where T.Index: Hashable {
    private let bindingArray: Binding<T>
    private let array: T
    private let content: (Binding<T.Element>) -> C

    init(_ bindingArray: Binding<T>, _ array: T, @ViewBuilder content: @escaping (Binding<T.Element>) -> C) {
        self.bindingArray = bindingArray
        self.array = array
        self.content = content
    }

    var body: some View {
        ForEach(array.indices, id: \.self) { index in
            Safe(bindingArray, index: index) {
                content($0)
            }
        }
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-11-25
    • 2017-03-05
    • 2014-12-19
    相关资源
    最近更新 更多