SwiftUI @Observable视图模型重复初始化详解及解决
2025-01-04 00:21:12
SwiftUI 中 @Observable 导致的视图模型重复初始化问题
SwiftUI 中使用 @Observable
管理应用状态是常见的实践,但有时可能会遇到视图模型 (ViewModel) 重复初始化的情况。与之前的 @StateObject
和 ObservableObject
组合相比,@Observable
的行为略有不同,这种不同可能导致意外的生命周期问题。我们通过以下实例分析其背后的原因和有效的应对方法。
问题分析
考虑如下 SwiftUI 代码:
struct ContentView: View {
@State var text = ""
var body: some View {
TextField("Enter text", text: $text)
SubView()
}
}
@Observable class SubViewModel {
var text: String = "Hello, World!"
init() {
print("init SubViewModel")
}
}
struct SubView: View {
@State private var viewModel: SubViewModel
init() {
_viewModel = State(wrappedValue: SubViewModel())
}
var body: some View {
Text(viewModel.text)
}
}
在这段代码中,ContentView
中有一个 @State
变量 text
。当用户在 TextField
中输入时, text
的值发生改变,SwiftUI 将重新构建 ContentView
的视图。 SubView
作为 ContentView
的子视图也会重新创建,并执行其初始化函数,每次都重新创建 viewModel
,导致 SubViewModel
的 init()
被重复调用。与 ObservableObject
和 @StateObject
的搭配相比,这是一个关键差异,也是问题产生的根源。
@State
的特性是用于存储视图本地的、轻量级的状态值,并不能管理跨视图的状态,视图一旦销毁就会重新初始化,这就意味着即使使用 @State
保存视图模型,也不能保证模型的生命周期不受视图本身影响,每次视图重绘都会生成新的模型。
解决方案
了解问题所在之后,接下来探讨一些解决方案,来管理 SubViewModel
的生命周期。
使用 @StateObject
和 @ObservedObject
(适用旧版本和混合使用场景)
@StateObject
旨在解决这种视图重新渲染导致视图模型重新创建的问题。 ObservableObject
和 @StateObject
是 Swift UI 中的一个搭配使用的设计。 @StateObject
的职责是在首次初始化视图时创建并保持实例。如果你的项目还在使用 ObservableObject
,这会是一个安全和可靠的选择。
class SubViewModel: ObservableObject {
@Published var text: String = "Hello, World!"
init() {
print("init SubViewModel")
}
}
struct SubView: View {
@StateObject private var viewModel = SubViewModel()
var body: some View {
Text(viewModel.text)
}
}
在这个调整过的例子里,SubView
使用 @StateObject
来持有 SubViewModel
。 SwiftUI 会确保该 viewModel
的生命周期绑定到该视图。即使 ContentView
由于 TextField
的更新而发生刷新,SubView
以及其中的 @StateObject
创建的 viewModel
将不会重新初始化。这种方法适用于将已有的 ObservableObject
模型平滑迁移过来。
原理: @StateObject
在首次创建时保留 ObservableObject
,并且只要视图保持活跃,就不会将其销毁,实现了生命周期的绑定,解决了重绘重新初始化的问题。
使用 @ObservedObject
并向上层传递实例 (适用于多个子视图共享)
在某些场景中,如果多个视图都需要访问同一视图模型实例,可以将该实例定义在它们共同的父视图,并使用 @ObservedObject
将其传递下去,而不是在每个子视图都创建新的实例。
struct ContentView: View {
@State var text = ""
@StateObject var sharedViewModel = SubViewModel()
var body: some View {
TextField("Enter text", text: $text)
SubView(viewModel: sharedViewModel)
AnotherSubView(viewModel: sharedViewModel)
}
}
@Observable class SubViewModel {
var text: String = "Hello, World!"
init() {
print("init SubViewModel")
}
}
struct SubView: View {
@ObservedObject var viewModel: SubViewModel
var body: some View {
Text(viewModel.text)
}
}
struct AnotherSubView: View {
@ObservedObject var viewModel: SubViewModel
var body: some View {
Text("AnotherView: \(viewModel.text)")
}
}
在这个示例里, SubViewModel
的实例是在 ContentView
使用 @StateObject
进行初始化。然后使用 @ObservedObject
传递到 SubView
和 AnotherSubView
中。 所有视图共享的是同一模型实例,并且不会在父视图重建时导致重复初始化。
原理: @ObservedObject
只会在视图及其 body
刷新的时候重新调用 body
相关的闭包方法,当上层视图重新构建,而下层绑定了模型的 @ObservedObject
则可以安全保持上层传递的模型引用。
使用环境对象(@EnvironmentObject) (适合在视图层级深的地方使用)
使用 @EnvironmentObject
可以在多个嵌套视图间方便的传递和共享模型。 ContentView
需要将该模型实例添加到环境中,所有它的子视图就可以访问。
@main
struct MyApp: App {
@StateObject var viewModel = SubViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewModel)
}
}
}
struct ContentView: View {
@State var text = ""
var body: some View {
TextField("Enter text", text: $text)
SubView()
}
}
@Observable class SubViewModel {
var text: String = "Hello, World!"
init() {
print("init SubViewModel")
}
}
struct SubView: View {
@EnvironmentObject var viewModel: SubViewModel
var body: some View {
Text(viewModel.text)
}
}
@EnvironmentObject
特别适用于在复杂的视图层级中管理状态。
原理: EnvironmentObject
可以通过环境,实现深层次嵌套的视图进行跨层级的传值。子视图无需关心对象来自哪里,只要在环境中,通过 @EnvironmentObject 即可进行访问。
安全提示
- 避免在
init()
中执行复杂的逻辑 :ViewModel 的初始化应该简单快捷。 避免在这里加载数据,而是通过单独的方法,或Task
进行异步的数据加载。这有助于保持代码简洁。 - 始终测试你的 ViewModel 生命周期 :确保测试各种情况,比如当应用程序在前后台切换、屏幕方向改变等,观察视图模型是否表现如预期,避免内存泄露或不必要的数据重新加载。
总结
选择正确的状态管理方法是 SwiftUI 开发的关键一环。理解 @Observable
与 @StateObject
、 @ObservedObject
以及环境对象的区别,并结合具体的使用场景来管理应用的复杂状态至关重要。 通过使用本篇文章所提及的正确方法,可以规避 @Observable
可能带来的问题,并构建出更高效的应用。
选择合适的方法有助于避免 SwiftUI 中视图模型不必要的重建,并且管理好应用程序的性能。