如何在ObservableObject上触发onChange?为何当前代码未生效?
问题:ObservableObject 属性修改时未触发 onChange 回调
假设我们有一个实现了Equatable协议的ObservableObject视图模型,包含多个@Published属性:
class ViewModel: ObservableObject, Equatable { static func == (lhs: ViewModel, rhs: ViewModel) -> Bool { lhs.value == rhs.value && lhs.anotherProperty1 == rhs.anotherProperty1 && lhs.anotherProperty2 == rhs.anotherProperty2 && lhs.anotherProperty3 == rhs.anotherProperty3 } @Published var value = 0 @Published var anotherProperty1 = "foo" @Published var anotherProperty2 = "bar" @Published var anotherProperty3 = "baz" }
我们期望ViewModel的任意属性修改时都能触发onChange回调,但在如下DemoView中,点击按钮修改value属性时,绑定viewModel的onChange并未触发:
struct DemoView: View { @StateObject var viewModel = ViewModel() var body: some View { Button { viewModel.value += 1 } label: { Text("Increment value \(viewModel.value.formatted())") } .onChange(of: viewModel) { _ in debugPrint("viewModel did change") } // 未触发回调 } }
原因分析
onChange(of: viewModel)监听的是视图模型实例的引用变化,而非实例内部属性的变化。修改viewModel.value时,viewModel本身的内存地址(引用)没有改变,SwiftUI就认为它没有“变化”,不会触发回调。- 虽然你实现了
Equatable协议定义了内容相等的规则,但SwiftUI对引用类型的onChange判断,默认是比较引用地址,不会调用==方法来判断内容是否变化。
解决方法
方法一:合并所有属性的变化信号
在ViewModel中创建一个合并所有@Published属性变化的Publisher,然后在视图中用onReceive监听:
修改ViewModel:
class ViewModel: ObservableObject, Equatable { static func == (lhs: ViewModel, rhs: ViewModel) -> Bool { lhs.value == rhs.value && lhs.anotherProperty1 == rhs.anotherProperty1 && lhs.anotherProperty2 == rhs.anotherProperty2 && lhs.anotherProperty3 == rhs.anotherProperty3 } @Published var value = 0 @Published var anotherProperty1 = "foo" @Published var anotherProperty2 = "bar" @Published var anotherProperty3 = "baz" // 合并所有属性的变化信号,任意属性修改都会发送通知 var anyPropertyChanged: AnyPublisher<Void, Never> { Publishers.Merge4( $value.map { _ in }, $anotherProperty1.map { _ in }, $anotherProperty2.map { _ in }, $anotherProperty3.map { _ in } ) .eraseToAnyPublisher() } }
修改DemoView:
struct DemoView: View { @StateObject var viewModel = ViewModel() var body: some View { Button { viewModel.value += 1 } label: { Text("Increment value \(viewModel.value.formatted())") } .onReceive(viewModel.anyPropertyChanged) { debugPrint("viewModel did change") } } }
方法二:监听所有属性的组合值
直接在onChange中监听ViewModel所有属性的组合元组,只要任意属性变化,组合值就会更新,从而触发回调:
struct DemoView: View { @StateObject var viewModel = ViewModel() var body: some View { Button { viewModel.value += 1 } label: { Text("Increment value \(viewModel.value.formatted())") } .onChange(of: (viewModel.value, viewModel.anotherProperty1, viewModel.anotherProperty2, viewModel.anotherProperty3)) { _ in debugPrint("viewModel did change") } } }
这种方法无需修改ViewModel,但属性较多时代码会比较繁琐。
方法三:手动触发对象变化通知
利用ObservableObject的objectWillChange发送器,给每个@Published属性添加didSet钩子,手动发送对象变化通知:
修改ViewModel:
class ViewModel: ObservableObject, Equatable { static func == (lhs: ViewModel, rhs: ViewModel) -> Bool { lhs.value == rhs.value && lhs.anotherProperty1 == rhs.anotherProperty1 && lhs.anotherProperty2 == rhs.anotherProperty2 && lhs.anotherProperty3 == rhs.anotherProperty3 } @Published var value = 0 { didSet { objectWillChange.send() } } @Published var anotherProperty1 = "foo" { didSet { objectWillChange.send() } } @Published var anotherProperty2 = "bar" { didSet { objectWillChange.send() } } @Published var anotherProperty3 = "baz" { didSet { objectWillChange.send() } } }
这样修改任何属性时,都会额外发送对象变化通知,SwiftUI会认为整个ViewModel发生了变化,从而触发onChange(of: viewModel)的回调。
内容的提问来源于stack exchange,提问作者parapote
相关产品推荐
相关产品推荐

