返回

SwiftUI @Observable视图模型重复初始化详解及解决

IOS

SwiftUI 中 @Observable 导致的视图模型重复初始化问题

SwiftUI 中使用 @Observable 管理应用状态是常见的实践,但有时可能会遇到视图模型 (ViewModel) 重复初始化的情况。与之前的 @StateObjectObservableObject 组合相比,@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,导致 SubViewModelinit() 被重复调用。与 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 传递到 SubViewAnotherSubView 中。 所有视图共享的是同一模型实例,并且不会在父视图重建时导致重复初始化。

原理: @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 即可进行访问。

安全提示

  1. 避免在 init() 中执行复杂的逻辑 :ViewModel 的初始化应该简单快捷。 避免在这里加载数据,而是通过单独的方法,或 Task 进行异步的数据加载。这有助于保持代码简洁。
  2. 始终测试你的 ViewModel 生命周期 :确保测试各种情况,比如当应用程序在前后台切换、屏幕方向改变等,观察视图模型是否表现如预期,避免内存泄露或不必要的数据重新加载。

总结

选择正确的状态管理方法是 SwiftUI 开发的关键一环。理解 @Observable@StateObject@ObservedObject 以及环境对象的区别,并结合具体的使用场景来管理应用的复杂状态至关重要。 通过使用本篇文章所提及的正确方法,可以规避 @Observable 可能带来的问题,并构建出更高效的应用。

选择合适的方法有助于避免 SwiftUI 中视图模型不必要的重建,并且管理好应用程序的性能。