SwiftUI 中 @ObservedObject 与 @StateObject 的区别
source link: https://justinyan.me/post/5926
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
在之前的学习笔记《SwiftUI学习笔记04 – 如何调试SwiftUI?》有提到@ObservedObject
容易导致View刷新被重复创建的问题。其中有一部分是我使用不当导致的。今天我们来分析下SwiftUI 中 @ObservedObject
与 @StateObject
的区别。
1. 生命周期不同
我们先来看一个常规的操作:
final class DebugViewModel: ObservableObject {
@Published var items: [String] = ["001", "002", "003"]
@Published var count = 5
init() {
print("DebugViewModel init \(Unmanaged.passUnretained(self).toOpaque())")
}
}
struct DebugView: View {
@ObservedObject private var viewModel = DebugViewModel()
var body: some View {
#if DEBUG
let _ = Self._printChanges()
#endif
NavigationStack {
DebugViewSubview()
List {
ForEach((1...viewModel.count), id: \.self) {
Text("\($0)")
}
}
.listStyle(.plain)
.toolbar {
ToolbarItem {
Button("+1") {
viewModel.count += 1
}
}
}
}
}
}
#Preview {
DebugView()
}
非常完美,DebugViewModel
只会被创建一次,点+1
按钮改变DebugViewModel
的属性,自己就跟着刷新了。但如果我们引入一个DebugViewSubview
,问题就来了。
final class DebugViewSubviewModel: ObservableObject {
@Published var count = 1
init() {
print("DebugViewSubviewModel init \(Unmanaged.passUnretained(self).toOpaque())")
}
}
struct DebugViewSubview: View {
@ObservedObject private var viewModel = DebugViewSubviewModel()
var body: some View {
Text("I'm a Subview. Count: \(viewModel.count)")
}
}
然后// 在DebugView的List上面加一个DebugViewSubview
NavigationStack {
DebugViewSubview() // <-这里
//…
}
我们看到DebugViewSubviewModel
的创建时机是DebugViewSubview
创建的时候。这时候我们再点+1
按钮,SwiftUI在刷新时就会倾向于重新创建View
。SwiftUI的View
都是轻量的Struct
,重新创建与绘制理论上应该是很高效的。但是这时候DebugViewSubviewModel
也会随着View
的创建而被重新创建一遍。每点一次+1
按钮就会重新创建一次。
DebugView: _viewModel changed.
DebugViewSubviewModel init 0x0000000281c129c0
DebugView: _viewModel changed.
DebugViewSubviewModel init 0x0000000281ca44c0
DebugView: _viewModel changed.
DebugViewSubviewModel init 0x0000000281c9bc40
在之前的文章中,我们提到可以通过EquatableView
或者Equatable
来规避部分Subview的重绘。但上面这种用法其实是错误的,我们应该使用@StateObject
而不是@ObservedObject
。我们把上面所有代码都不变,只改@StateObject
:
@StateObject private var viewModel = DebugViewSubviewModel()
这样再点+1
按钮,就不会一直创建了:
DebugViewSubviewModel init 0x0000000282318500
DebugView: _viewModel changed.
DebugView: _viewModel changed.
DebugView: _viewModel changed.
2. 什么时候用@StateObject和@ObservedObject?
SwiftUI提供了@StateObject
, @ObservedObject
和@EnvironmentObject
这几种常用的Property Wrapper。
大部分情况下,我们用@StateObject
来作为一个View
的数据来源,通过@StateObject
初始化的Property,即便View
多次被刷新,其初始化方法也只会被调用一次。这样我们就不用担心@StateObject
里额外添加的各种通知逻辑,异步读写数据逻辑被重复调用。所以上述例子中,DebugView
和DebugViewSubview
创建的两个ViewModel
都只涉及View自身的数据,都应该使用@StateObject
而不是ObservedObject
声明。
当我们需要把Parent View的的@StateObject
传递给Subview的时候,我们可以在Subview声明@ObservedObject
。比如上述例子中,如果DebugViewSubview
需要用到DebugView
的DebugViewModel
,那么我们可以这么写:
struct DebugViewSubview: View {
@StateObject private var viewModel = DebugViewSubviewModel()
@ObservedObject private var parentViewModel: DebugViewModel
init(parentViewModel: DebugViewModel) {
self.parentViewModel = parentViewModel
}
var body: some View {
Text("I'm a Subview. Count: \(viewModel.count)")
Text("parentViewModel Count: \(parentViewModel.count)")
}
}
这样,parentViewModel
在DebugView
中被+1
了之后,DebugViewSubview
也会跟着变化。这就是@ObservedObject
的真正用途: 在不同的View
之间传递ObservableObject
。
DebugViewModel init 0x0000000283289dd0
DebugView: @self, @identity, _viewModel changed.
DebugViewSubviewModel init 0x0000000283ce6140
DebugView: _viewModel changed.
DebugView: _viewModel changed.
这时候我们发现,有些数据源我希望挂在App
上为全局使用,比如一个帐号是否已登录之类的。通常我们会在App
上创建一个@StateObject
,但如果我有很多个View
都需要用到这个数据,那我岂不是得创建很多个@ObservableObject
然后一层层传下去?为了方便大家开发,SwiftUI提供了@EnvironmentObject
。用于在View
层级上传递数据。比如一个典型的SwiftUI App入口:
@main
struct SomeApp: App {
@StateObject private var appInfo: AppInfo
init() {
self._appInfo = StateObject(wrappedValue: AppInfo.shared)
}
var body: some Scene {
WindowGroup {
MainView()
.environmentObject(appInfo) // <-- 这里
}
}
}
在上述代码中,我把appInfo
通过.environmentObject()
传递给了MainView
,这样它的所有Subviews都可以共享这个appInfo
实例。只需要在MainView
中这样写:
import SwiftUI
struct MainView: View {
@EnvironmentObject private var appInfo: AppInfo
// …
}
这样就能获得由App
全局负责管理生命周期的appInfo
。MainView
的Subviews如果也想用到,只要依法炮制即可。
3. 新的 @Observable Macro
在WWDC23中,Apple推出了SwiftUI 5的新特性,用@Observable
宏代替了@ObservedObject
与@Published
声明。我们可以把上面DebugViewSubview
的代码改写为:
// 这里用 @Observable 声明整个类
@Observable final class DebugViewSubviewModel{
var count = 1 // <-- 这里删掉了之前的 @Published
@ObservationIgnored // <-- 不需要发出变更的通知用 @ObservationIgnored 修饰
var propertyWontSendWillChange = 0
init() {
print("DebugViewSubviewModel init \(Unmanaged.passUnretained(self).toOpaque())")
}
}
struct DebugViewSubview: View {
@State private var viewModel = DebugViewSubviewModel()
// …
}
@Observable
是一个Macro,在Xcode 15中我们可以通过Editor -> Expand Macro
来看这它生成了什么代码,在此之前,我们需要确保有import Observation
。
可以看到生成的代码很简单,_$observationRegistrar
用来保存监听变化的对象,当@ObservationTracked
属性发生变化时回调给监听者。func access<Member>()
是getter,withMutation<Member, MutationResult>
是setter。然后还让DebugViewSubviewModel
遵循了Observation.Observable
协议。
我们展开@ObservationTracked
Macro可以看到:
每个@ObservationTracked
property会生成一个private
property,getter/setter用的就是上面那两个,从而实现监听和通知。
使用时我们改用@State
代替@StateObject
,如果是@Binding
类型的,就用@Bindable
声明。比如:
struct DebugViewSubview: View {
@Bindable private var viewModel = DebugViewSubviewModel()
// …
}
这些Macro的更新是有限制条件的: SwiftUI 5 only。所以如果你打算开发只支持 iOS 17 以上的 App,那就可以把所有的 @StateObject
全部替换为 @Observable
Macro实现了。
但经过我的测试,如下声明的DebugViewSubview
,因为DebugViewSubviewModel
的Property Wrapper从@StateObject
改为@State
了,也就失去了@StateObject
只会创建一次的生命周期管理特性。于是每一次Parent View的+1
按钮点了,DebugViewSubviewModel
就会被重新创建,跟我们使用@ObservedObject
声明一样。
@Observable
final class DebugViewSubviewModel {
var count = 1
init() {
print("DebugViewSubviewModel init \(Unmanaged.passUnretained(self).toOpaque())")
}
}
struct DebugViewSubview: View {
@State private var viewModel = DebugViewSubviewModel()
//…
}
上述声明的代码,在Parent View里的+1
按钮被按下以后,DebugViewSubviewModel
会被不断创建。
DebugView: @dependencies changed.
DebugViewSubviewModel init 0x00000002814eb7a0
DebugViewSubview: @self changed.
DebugView: @dependencies changed.
DebugViewSubviewModel init 0x000000028149a7c0
DebugViewSubview: @self changed.
目前我没有找到什么好办法来规避使用@Observable
下Subviews的重绘与标记为@State
的ViewModel的重建,只能说这种刷新是设计如此。
综上所述,现状我还是使用@StateObject
为主,如果未来要迁移到@Observable
Macro我就得想办法解决Subviews里的ViewModel
会一直被重新创建的问题。比如说,以后所有的ViewModel
都需要跟View
的生命周期分离,不可以由当前View
来创建,而是谁能决定它被刷新,就由谁来创建。
比如DebugViewSubview
它的ViewModel
就交给DebugView
来创建,这样它就不会因为View
被刷新而频繁init。但是这样也不好,因为DebugViewSubview
往下可能还有Subviews呢?我要把所有的Subviews ViewModel全部提到最上面一层来吗?用Environment
确实可以做到,但我也不知道这样是否合理。
简单来说,SwiftUI 5这次带来的Observation
升级,是直接把在View的生命周期内只会创建一次的@StateObject特性给砍掉了,必然会对我们原有的设计与写法产生影响。另一个方面想,也许是我们熟悉的MVC/MVVM不适合SwiftUI的设计理念,探索TCA架构能提供一个新的解决方案呢?
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK