You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 21:22:25