从 SwiftUI 的列表中删除列表元素

Posted

技术标签:

【中文标题】从 SwiftUI 的列表中删除列表元素【英文标题】:Deleting list elements from SwiftUI's 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) 【参考方案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。

以上是关于从 SwiftUI 的列表中删除列表元素的主要内容,如果未能解决你的问题,请参考以下文章

从 SwiftUI 的列表中删除列表元素

如果列表行在 SwiftUI 中包含文本字段,则无法从 foreach 中删除元素

SwiftUI + Realm:从列表中删除一行会导致异常“RLMException”,原因:“对象已被删除或失效。”

删除嵌套属性中的元素时更新 SwiftUI 列表

尝试删除最后一个元素时,带有子项的 SwiftUI (2.0) 列表崩溃

删除最后一个数组元素时,SwiftUI列表ForEach索引超出范围