SwiftUI:在第二个视图修改@Binding对象引用不生效的问题咨询
嘿,我来帮你拆解这个问题,先说说为啥会出现这种不符合预期的情况,再给你几个比临时方案更优雅的解决办法。
问题根源分析
你遇到的核心问题,和@Binding在ObservableObject里的工作机制直接相关:
@Binding不会自动触发ObservableObject的更新通知:SwiftUI的ObservableObject只会主动监听@Published属性的变化,@Binding属性的修改不会自动触发objectWillChange事件。你手动调用viewModel.objectWillChange.send()的时机也不对——你是在给model赋值新实例之前发送的通知,这时候视图刷新拿到的还是旧值,等新值赋上后又没有再次通知,所以SecondView的UI不会更新。TestModel是普通引用类型:它不是ObservableObject,所以修改它的text属性时,外部的@Published var model不会自动感知变化,只能靠手动发通知;而替换整个实例时,虽然外部的@Published会触发FirstView更新,但SecondView因为依赖的是VM里的@Binding,VM没有监听这个Binding的变化来同步视图,所以SecondView自己的UI不会刷新。
你的临时方案用额外的@Published属性同步状态,虽然能解决问题,但属于冗余的手动同步,容易出错也不够优雅。
最佳解决方案
下面给你三种更合理的处理方式,你可以根据自己的场景选择:
方案一:让TestModel成为ObservableObject(最推荐)
从根源上解决状态更新问题,把TestModel改成可观察对象,这样它的属性变化会自动通知视图,同时实例替换的逻辑也会更顺畅:
class TestModel: ObservableObject { @Published var text: String? init(text: String?) { self.text = text } }
然后如果SecondView逻辑不复杂,可以直接去掉VM,用@Binding传递状态:
struct SecondView: View { @Binding var model: TestModel? var body: some View { VStack { Text(model?.text ?? "n/a") .padding() Button("Edit model") { model?.text = "Edited from screen 2" } .padding() Button("New model") { model = TestModel(text: "New model from screen 2") } .padding() } } }
如果需要FirstView在sheet打开时也能实时感知TestModel的属性变化,可以在FirstView的Text上添加监听:
struct FirstView: View { @StateObject var viewModel = FirstViewVM() var body: some View { VStack { Text(viewModel.model?.text ?? "n/a") .padding() // 监听TestModel的内部变化,触发FirstView更新 .onReceive(viewModel.model?.objectWillChange ?? Empty()) { _ in viewModel.objectWillChange.send() } Button("New model") { viewModel.model = TestModel(text: "New model from screen 1") } .padding() Button("Show screen 2") { viewModel.showSecondView = true } .padding() } .sheet(isPresented: $viewModel.showSecondView) { NavigationView { SecondView(model: $viewModel.model) } } } }
方案二:用Combine同步@Binding和@Published(保留VM的场景)
如果你的SecondView必须用ViewModel,可以用Combine自动同步@Binding和VM里的@Published属性,避免手动同步的冗余:
class SecondViewVM: ObservableObject { @Published var model: TestModel? private var cancellables = Set<AnyCancellable>() init(model: Binding<TestModel?>) { // 初始化同步外部值 self.model = model.wrappedValue // 监听外部Binding变化,同步到内部@Published model.projectedValue .assign(to: \.model, on: self) .store(in: &cancellables) // 监听内部@Published变化,同步回外部Binding $model .assign(to: model) .store(in: &cancellables) } } struct SecondView: View { @StateObject var viewModel: SecondViewVM var body: some View { VStack { Text(viewModel.model?.text ?? "n/a") .padding() Button("Edit model") { viewModel.model?.text = "Edited from screen 2" } .padding() Button("New model") { viewModel.model = TestModel(text: "New model from screen 2") } .padding() } } }
这样不管是外部修改还是内部修改model,两边都会自动同步,而且@Published会自动触发视图更新,不需要手动调用objectWillChange。
方案三:直接在VM中使用@Binding并正确触发通知
如果不想改Model也不想用Combine,那可以调整手动触发通知的时机,在赋值新实例之后发送:
class SecondViewVM: ObservableObject { @Binding var model: TestModel? init(model: Binding<TestModel?>) { _model = model } } struct SecondView: View { @StateObject var viewModel: SecondViewVM var body: some View { VStack { Text(viewModel.model?.text ?? "n/a") .padding() Button("Edit model") { viewModel.model?.text = "Edited from screen 2" // 修改属性后发送通知 viewModel.objectWillChange.send() } .padding() Button("New model") { viewModel.model = TestModel(text: "New model from screen 2") // 赋值新实例后发送通知 viewModel.objectWillChange.send() } .padding() } } }
这个方案最简单但不够优雅,因为每次修改都要手动发通知,容易遗漏。
总结
你的临时方案本质是手动同步状态,而上面的方案一(让Model成为ObservableObject)是最推荐的,它从根源上规范了状态管理;如果必须保留VM,方案二的Combine同步更可靠;方案三适合快速临时修复。
内容的提问来源于stack exchange,提问作者Stefan Nestorov

