SwiftUI似乎有一个令人讨厌的限制,即在绑定到每个元素以传递到子视图时,很难创建List
或ForEach
。
我见过的最常建议的方法是遍历索引,并与$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)") } )
}
}
}
什么是处理从列表中删除的正确方法,同时仍保持使用绑定修改其元素的能力?
答案 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)") } )
}
}