You need to enable JavaScript to run this app. SwiftUI 视图模型数组内模型更新时,选中计数与总价格视图不刷新
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.31 21:36:03