SwiftUI跨ViewModel属性观测异常现象的技术问询
场景与实验背景
通常在SecondView中监听FirstView初始化的FirstViewModel属性时,会把@StateObject修饰的ViewModel传入SecondView并声明为@ObservedObject。为了让视图仅负责视图逻辑,我尝试将FirstViewModel传入SecondView后初始化SecondViewModel,直接在SecondViewModel中访问其数据,做了如下实验:
实验代码
FirstViewModel
class FirstViewModel: ObservableObject { @Published var isGreen = false }
FirstView
struct FirstView: View { @StateObject var firstVM = FirstViewModel() var body: some View { NavigationStack { Form { Toggle("First View", isOn: $firstVM.isGreen) NavigationLink { SecondView(firstVM: firstVM) } label: { Text("Go to second View") } } } } }
SecondViewModel
class SecondViewModel: ObservableObject { @Published var firstVM: FirstViewModel init(firstVM: FirstViewModel) { self.firstVM = firstVM } }
SecondView
struct SecondView: View { @StateObject var secondVM: SecondViewModel // @ObservedObject var firstVM: FirstViewModel init(firstVM: FirstViewModel) { _secondVM = StateObject(wrappedValue: SecondViewModel(firstVM: firstVM)) // self.firstVM = firstVM } var body: some View { Form { Section { Button("Button") { secondVM.firstVM.isGreen.toggle() } Toggle("Toggle", isOn: $secondVM.firstVM.isGreen) Rectangle() .frame(height: 4) .foregroundColor(secondVM.firstVM.isGreen ? .green : .red) } header: { Text("Published Property") } } } }
实验配置
FirstViewModel包含@Published布尔属性isGreen,在FirstView中初始化SecondViewModel包含@Published修饰的FirstViewModel类型属性,初始化时赋值SecondView初始化时接收FirstViewModel并传入SecondViewModelSecondView仅通过SecondViewModel访问isGreen属性- 通过注释/取消注释
SecondView中的@ObservedObject var firstVM: FirstViewModel观察行为
观察结果
- 按钮和Toggle都能修改
isGreen属性,关闭SecondView后FirstView的Toggle会同步变化 - 使用按钮时
SecondView中的Rectangle不改变颜色,但使用Toggle(绑定属性)时会改变 - 取消注释
SecondView中的@ObservedObject firstVM后,即使未使用该实例,按钮操作也能让SecondView响应变化,SecondViewModel可通过自身的@Published firstVM观测FirstViewModel的变化
补充说明:将SecondViewModel中的@Published var firstVM改为let firstVM,仍出现上述第3点的现象,仅无法使用Toggle(因需绑定属性)
核心问题
- 为何
SecondView中ObservedObject的存在能让SecondViewModel监听FirstViewModel的变化? - 改用
let修饰firstVM为何也有效? - 为何绑定
isGreen到Toggle时视图会自动响应,这是否与类实例存储在堆内存有关?
问题解答
1. ObservedObject让SecondViewModel间接监听变化的原因
SwiftUI的视图会自动追踪所有用@ObservedObject、@StateObject声明的ObservableObject实例的objectWillChange发布者。当你在SecondView中声明@ObservedObject var firstVM: FirstViewModel时,视图会订阅这个firstVM的状态变化——只要firstVM的@Published属性更新,就会触发SecondView的重绘。
此时即使你在视图里没直接用这个firstVM实例,视图的重绘逻辑会被触发,而SecondView的body里用到了secondVM.firstVM.isGreen,重绘时会读取最新的isGreen值,所以看起来像是SecondViewModel“监听”到了变化,本质是视图被ObservedObject的更新触发了重绘,刷新了UI展示的状态。
2. let修饰firstVM仍有效的原因
FirstViewModel是引用类型(类),不管是用var还是let存储,SecondViewModel里保存的都是指向堆内存中同一个FirstViewModel实例的引用。当你在SecondView中声明@ObservedObject firstVM时,订阅的是这个引用指向的实例的变化,和SecondViewModel里是var还是let无关——只要实例本身的@Published属性更新,就会触发视图重绘,进而让SecondView展示最新状态。
至于无法使用Toggle,是因为$secondVM.firstVM.isGreen需要firstVM是可写的(var),let修饰的属性无法生成绑定,所以Toggle的双向绑定会失效。
3. Toggle绑定自动响应的原因
当你用$secondVM.firstVM.isGreen绑定Toggle时,这个绑定本质上是直接关联到FirstViewModel的@Published属性的发布者。SwiftUI的绑定类型(Binding)内部会自动订阅属性的变化,同时在用户操作Toggle时更新属性值。
当你操作Toggle时,不仅会修改isGreen的值,还会触发FirstViewModel的objectWillChange发布事件,这个事件会被绑定本身监听,进而直接触发视图的重绘,所以Rectangle会立即更新颜色。这和类实例存在堆内存有关——因为引用类型的实例在堆上,绑定和视图都指向同一个实例,状态变化能被实时感知。
内容的提问来源于stack exchange,提问作者Khaled Sh

