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

时间:2020-07-24 18:08:27

标签: swift swiftui

SwiftUI似乎有一个令人讨厌的限制,即在绑定到每个元素以传递到子视图时,很难创建ListForEach

我见过的最常建议的方法是遍历索引,并与$arr[index]进行绑定(实际上,当它们删除Binding时,与suggested by Apple类似符合Collection):

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

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

3 个答案:

答案 0 :(得分:4)

利用来自@ 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")
      }
   }
}

答案 1 :(得分: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)") })
        }
    }
}

答案 2 :(得分:1)

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