删除行时,SwiftUI 列表行视图/视图模型未取消初始化

问题描述 投票:0回答:1

当从列表中添加和删除行时,我无法理解 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

这很奇怪也很烦人:(

ios swiftui mvvm swiftui-list deinit
1个回答
0
投票

您的困惑是有道理的,可以归因于多种原因。

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
中的内容从可见区域完全消失时,就会发生这种情况。

© www.soinside.com 2019 - 2024. All rights reserved.