SwiftUI中@Observable视图模型绑定值时避免全局视图失效的方法
从@ObservedObject迁移到@Observable宏后的视图失效问题
将视图模型从@ObservedObject协议迁移到@Observable宏,本应减少模板代码(比如不再需要@Published属性包装器),同时利用@Observable的智能失效机制提升应用性能——只更新真正需要刷新的视图。但在绑定@Observable视图模型中的字符串值时,却出现了相反的情况:
- 直接通过
$viewModel.text绑定视图模型的字符串值,会导致所有视图失效,包括完全不依赖该文本值的视图 - 按照官方文档尝试为视图模型创建
@Bindable引用时,出现编译错误 - 引入封装了@Bindable视图模型的子视图可以解决问题(此时仅依赖文本值的视图会失效),但这种方式会再次增加大量冗余代码,只是把
@Published属性包装器换成了不必要的子视图
以下是复现该问题的简单示例代码,在模拟器运行时,let _ = Self._printChanges()会在日志中显示哪些视图被失效。当前使用Xcode 15.4,目标iOS 17+:
import SwiftUI // MARK: - ContentView struct ContentView: View { var body: some View { let _ = Self._printChanges() NavigationStack { UserListView() .navigationTitle("View Invalidations...") } } } // MARK: - Model @Observable class User: Identifiable { var id = UUID() } // MARK: - Mock extension User { static func random() -> User { return .init() } static func randomLot() -> [User] { var result: [User] = .init() for _ in 1..<50 { result.append(.random()) } return result } } // MARK: - UserListView struct UserListView: View { // ✅ 使用@State,因为该视图是这个视图模型的数据源 @State var viewModel = ViewModel() // ❌ 直接初始化Bindable -> 使用文本框时,列表中的所有视图仍会失效 // @Bindable private var viewModel: ViewModel = .init() var body: some View { let _ = Self._printChanges() List { ForEach(viewModel.items) { UserRowView(item: $0) } } // ❌ 直接绑定视图模型的属性 -> 使用文本框时,列表中的所有视图会失效 .overlay(alignment: .bottom) { SomeCustomTextView(text: $viewModel.text) } // ❌ 在视图中创建@Bindable -> 编译错误 // .overlay(alignment: .bottom) { // VStack { // @Bindable var model = viewModel // SomeCustomTextView(text: $model.text) // // } // } // ✅ 在另一个视图中创建@Bindable // .overlay(alignment: .bottom) { // SomeOverLayView(viewModel: viewModel) // } } } // MARK: UserListView - ViewModel extension UserListView { @Observable class ViewModel { var items: [User] = User.randomLot() var text: String = "" } } // MARK: - SomeCustomTextView struct SomeCustomTextView: View { @Binding var text: String var body: some View { let _ = Self._printChanges() TextField("输入时能否不刷新所有行?", text: $text) .padding() .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 25.0)) .padding() .shadow(radius: 20) } } // MARK: - UserRowView struct UserRowView: View { @State private var viewModel: ViewModel init(item: User) { _viewModel = .init(wrappedValue: .init(item: item)) } var body: some View { let _ = Self._printChanges() Text(viewModel.user.id.uuidString) .frame(maxWidth: .infinity, alignment: .leading) } } // MARK: UserRowView - ViewModel extension UserRowView { @Observable class ViewModel { var user: User init(item: User) { self.user = item } } } // 🔍 MARK: - 备选方案,但不够理想 // ✅ 如果创建视图模型的@Bindable,输入文本时不会刷新行... // FIXME: 但这种方式会给生产应用带来大量额外代码 struct SomeOverLayView: View { var viewModel: UserListView.ViewModel var body: some View { let _ = Self._printChanges() @Bindable var model = viewModel SomeCustomTextView(text: $model.text) } } // ✅ 另一种方式是让视图直接依赖视图模型 // FIXME: 但同样会增加大量额外代码 struct SomeCustomTextViewV2: View { @Bindable var viewModel: UserListView.ViewModel var body: some View { let _ = Self._printChanges() TextField("测试", text: $viewModel.text) } } #Preview { ContentView() }
编辑1:视图模型的共享依赖似乎是问题的一部分;将UserRowView改为直接依赖User对象而非视图模型,也能解决问题:
struct UserRowView: View { var item: User var body: some View { let _ = Self._printChanges() Text(item.id.uuidString) .frame(maxWidth: .infinity, alignment: .leading) } }
但在更复杂的项目中,我们可能需要带有共享依赖的视图模型?而通过引入子视图并保留用户行视图模型可以解决问题,这说明应该有更优的方案...
编辑2:在视图body内直接创建@Bindable可以编译通过,但无法解决视图失效问题:
struct UserListView: View { @State var viewModel = ViewModel() var body: some View { let _ = Self._printChanges() // ❌ 在此处创建Bindable可以编译,但视图模型的text值变化时,仍会导致大量不必要的视图失效 @Bindable var viewModel = viewModel List { ForEach(viewModel.items) { UserRowView(item: $0) } } .overlay(alignment: .bottom) { // ❌ 虽然技术上仍在视图body内,但在此处添加@Bindable会导致编译错误 // @Bindable var viewModel = viewModel SomeCustomTextView(text: $viewModel.text) } // ✅ 在另一个视图中创建@Bindable // .overlay(alignment: .bottom) { // SomeOverLayView(viewModel: viewModel) // } } }
内容的提问来源于Stack Exchange,提问作者Julius
相关产品推荐
相关产品推荐

