从 SwiftUI 的列表中删除列表元素
Posted
技术标签:
【中文标题】从 SwiftUI 的列表中删除列表元素【英文标题】:Deleting list elements from SwiftUI's List 【发布时间】:2020-07-24 18:08:27 【问题描述】:SwiftUI 似乎有一个相当烦人的限制,这使得在获取每个元素的绑定以传递给子视图时很难创建 List
或 ForEach
。
我见过的最常见的建议方法是迭代索引,并获得与$arr[index]
的绑定(事实上,当他们删除Binding
与Collection
的一致性时,类似的东西是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) -> 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 中包含文本字段,则无法从 foreach 中删除元素
SwiftUI + Realm:从列表中删除一行会导致异常“RLMException”,原因:“对象已被删除或失效。”