当从列表中添加和删除行时,我无法理解 SwiftUI 如何管理列表行视图(及其关联的视图模型)。
在下面的小示例中,当从列表中手动删除该行时,视图模型
deinit()
不会执行。
struct ContentView: View {
@State var list: [String] = [
"A", "B", "C"
]
var body: some View {
List {
ForEach(list, id: \.self) { item in
RowView(item: item)
}
.onDelete(perform: { indexSet in
indexSet.forEach { list.remove(at: $0) }
})
}
}
}
struct RowView: View {
@State var vm: RowViewModel
init(item: String) {
vm = RowViewModel(item: item)
}
var body: some View {
Text(vm.item)
}
}
@Observable
class RowViewModel {
public let item: String
init(item: String) {
print("RowVM:Init \(item)")
self.item = item
}
deinit {
print("RowVM:Deinit \(item)")
}
}
当我启动应用程序时,我看到:
RowVM:Init A
RowVM:Init A
RowVM:Deinit A
RowVM:Init B
RowVM:Init B
RowVM:Deinit B
RowVM:Init C
RowVM:Init C
RowVM:Deinit C
似乎 SwiftUI 创建了 2 个新的行视图及其视图模型,然后删除了一个。我本以为只有 1 个。
然后当我滑动 C 行将其删除时,我观察到:
RowVM:Init A
RowVM:Init B
再次创建新的行视图 A 和 B 及其虚拟机,而无需取消初始化之前的虚拟机,但更糟糕的是,我没有看到:
RowVM:Deinit C
我必须停止应用程序才能看到完整的 deinit() 系列:
RowVM:Deinit C
RowVM:Deinit B
RowVM:Deinit B
RowVM:Deinit A
RowVM:Deinit A
这很奇怪也很烦人:(
您的困惑是有道理的,可以归因于多种原因。
1。
ForEach
可能会多次生成不必要的视图
使用
ForEach
时,可能会发生每个视图最初创建两次,不必要的视图然后立即被丢弃的情况。
据我所知,目前还没有直接的解决方案,你必须接受这种行为,直到苹果对此做出改变。
一般来说,这应该不是问题,因为 SwiftUI 视图的初始化必须始终是轻量级的。如果确实出现问题,这通常表明您正在
init
方法中执行某些操作,而您不应该这样做,例如在每次调用时创建一个繁重的视图模型实例。
下面详细介绍这一点。
2。使用
@Observable
模型作为 @State
属性
State
和StateObject
之间的一个很大的区别是StateObject
提供了一个init方法,其包装的值仅在需要创建时才被访问,这就是为什么它是一个autoclosure
。
如果
RowViewModel
是 ObservableObject
,RowView
看起来像这样:
struct RowView: View {
@StateObject private var vm: RowViewModel
var body: some View {
Text(vm.item)
}
init(item: String) {
_vm = .init(wrappedValue: RowViewModel(item: item))
}
}
使用此变体,不会出现每次调用
RowViewModel
时都会不必要地创建 RowView.init
的问题。仅当需要通过调用 RowViewModel
闭包创建新状态时,SwiftUI 才会创建 wrappedValue
。
考虑到1.中描述的
ForEach
问题,这仍然会导致创建不必要的视图,但不再创建不必要的RowViewModel
实例。
但是,如果您使用
Observable
宏,情况就会发生变化,因为 State.init 方法不使用 autoclosure
,因此每次调用时都会创建一个新的视图模型实例。
Apple 在开发者文档中建议将相应的
State
属性设置为可选,并通过 task
调用来初始化它
当 SwiftUI 实例化视图时,State 属性始终实例化其默认值。因此,在初始化默认值时,请避免副作用和性能密集型工作。例如,如果视图频繁更新,则每次视图初始化时分配新的默认对象可能会变得昂贵。相反,您可以使用 task(priority:_:) 修饰符推迟对象的创建,该修饰符仅在视图首次出现时调用一次
应用于您的案例,
RowView
可能看起来像这样:
struct RowView: View {
@State private var vm: RowViewModel?
private let item: () -> String
init(item: @autoclosure @escaping () -> String) {
self.item = item
}
var body: some View {
VStack {
if let item = vm?.item {
Text(item)
}
}
.task {
vm = RowViewModel(item: item())
}
}
}
当然,这不是一个现实世界的例子,因为
RowViewModel
在这种情况下并没有真正做任何重量级的事情。
但它向您展示了可以针对您的特定用例使用哪种模式。
3. SwiftUI
State
资源发布
现在您可能想知道为什么当相应的
deinit
消失/被删除时,RowViewModel
实例的 RowView
可能不会立即被调用。
这是由于
State
资源的内部管理。 SwiftUI 发布它们的确切时间点是内部实现细节,不应依赖。
例如,当
ScrollView
中的内容从可见区域完全消失时,就会发生这种情况。