SwiftUI 视图模型数组内模型更新时,选中计数与总价格视图不刷新
问题根因
这是典型的嵌套 ObservableObject 监听失效场景:
@Published仅会监听当前对象自身的属性变化,不会自动追踪引用类型数组内部元素的属性变更。ServiceSelectorModel是引用类型(class),ViewModel 中的services数组只有在发生「增删元素、替换整个元素」这类操作时,才会触发@Published的通知。单个 Model 内部的selected状态变化,完全不会通知 ViewModel,因此上层视图收不到刷新信号,不会重新计算选中数和总价。
修复方案
方案1:最小改动,手动转发子状态变化(兼容现有代码)
只需要给 ViewModel 加 Combine 订阅,监听每个子 Model 的状态变化,子状态变更时主动触发 ViewModel 的更新通知,原有业务代码完全不用改动:
import Combine class ServiceSelectorViewModel: ObservableObject { @Published var services = [ServiceSelectorModel]() // 存储订阅销毁包,避免内存泄漏 private var cancellables = Set<AnyCancellable>() init() { self.services = [ ServiceSelectorModel(serviceName: "SERVICE 1", price: 1.80), ServiceSelectorModel(serviceName: "SERVICE 2", price: 10.22), ServiceSelectorModel(serviceName: "SERVICE 3", price: 2.55) ] // 给每个子Model绑定状态监听 services.forEach { model in model.$selected .sink { [weak self] _ in // 子状态变化时,主动触发ViewModel的更新通知 self?.objectWillChange.send() } .store(in: &cancellables) } } }
方案2:符合MVVM规范的重构(适合长期维护)
把选中状态、选中数、总价这类和视图交互强相关的逻辑全部下沉到 ViewModel,Model 改为纯数据结构,逻辑更清晰,天生支持动态增删服务列表:
1. 把Model改为纯值类型,去掉交互状态
struct ServiceSelectorModel: Identifiable { let id = UUID() let serviceName: String let price: Double }
2. 重构ViewModel,统一管理交互状态
class ServiceSelectorViewModel: ObservableObject { // 静态服务列表,无状态 let services = [ ServiceSelectorModel(serviceName: "SERVICE 1", price: 1.80), ServiceSelectorModel(serviceName: "SERVICE 2", price: 10.22), ServiceSelectorModel(serviceName: "SERVICE 3", price: 2.55) ] // 直接用选中ID集合管理所有选中状态,由ViewModel持有 @Published var selectedIds = Set<UUID>() // 直接在ViewModel内部计算选中数、总价,避免视图层写业务逻辑 var selectedCount: Int { selectedIds.count } var totalPrice: Double { services.filter { selectedIds.contains($0.id) } .map { $0.price } .reduce(0, +) } // 统一提供切换选中状态的方法 func toggleService(with id: UUID) { if selectedIds.contains(id) { selectedIds.remove(id) } else { selectedIds.insert(id) } } }
3. 调整视图代码,直接绑定ViewModel的状态和方法
子Toggle视图直接接收ViewModel和单个服务模型:
struct ServiceToggleView: View { @ObservedObject var vm: ServiceSelectorViewModel let service: ServiceSelectorModel var body: some View { VStack(alignment: .center) { HStack { Text(service.serviceName) // 绑定ViewModel的选中状态 Toggle(isOn: Binding( get: { vm.selectedIds.contains(service.id) }, set: { _ in vm.toggleService(with: service.id) } )) { Text(String(format: "€ +%.2f", service.price)) .frame(maxWidth: .infinity, alignment: .trailing) } } .background(vm.selectedIds.contains(service.id) ? Color.yellow : Color.clear) } } }
主视图直接调用ViewModel的计算属性,无冗余计算逻辑:
struct ServiceSelectorView: View { @ObservedObject var serviceSelectorVM = ServiceSelectorViewModel() var body: some View { VStack { VStack(alignment: .leading) { ForEach(serviceSelectorVM.services) { service in ServiceToggleView(vm: serviceSelectorVM, service: service) } } Text("SELECTED: \(serviceSelectorVM.selectedCount)") Text(String(format: "TOTAL PRICE: €%.2f", serviceSelectorVM.totalPrice)) } } }
注意:如果后续需要动态增删服务列表,方案1需要在每次新增服务时额外给新的子Model绑定状态订阅,维护成本更高;方案2天然适配动态列表场景,扩展性更好。
内容的提问来源于stack exchange,提问作者elp
相关产品推荐
相关产品推荐

