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

SwiftUI MVVM模式下如何在视图模型间传递@Published值并支持修改

问题根因

你原代码的核心问题是通过Published(wrappedValue: viewModel.selectedPerson)创建了新的@Published容器,和父视图模型的属性属于完全独立的两个存储单元,修改其中一个不会触发另一个的更新,自然无法同步。
你之前尝试的@Binding、@EnvironmentObject方案不符合预期的原因是:前者是面向视图层的双向绑定载体,无法直接在视图模型层接入Combine数据流处理;后者需要将逻辑下沉到视图层,不符合MVVM职责划分要求。

规范实现方案

核心思路是保持单一数据源,仅在父子视图模型之间传递数据流通道和修改权限:

  1. 父视图模型的@Published属性作为唯一可信数据源
  2. 子视图模型初始化时接收两个入参:
    • 父属性的Publisher(即父@Published的投影值$xxx),用于订阅数据变化,接入Combine处理
    • 父属性的修改闭包,子视图模型需要修改数据时通过闭包通知父层更新,避免直接持有数据副本
  3. 子视图模型内部可以基于接收的Publisher做任意Combine操作,生成自身需要的视图驱动状态

修复后的Playground示例代码

import SwiftUI
import PlaygroundSupport
import Combine

class InitialViewModel: ObservableObject {
    @Published var selectedPerson: Person = Person(firstName: "", surname: "")
}

struct InitialView: View {
    @StateObject var viewModel = InitialViewModel()

    var body: some View {
        VStack(spacing: 20) {
            // 传入父层的Publisher和修改闭包
            ButtonView(
                selectedPersonPublisher: viewModel.$selectedPerson.eraseToAnyPublisher(),
                updateSelectedPerson: { [weak viewModel] newPerson in
                    viewModel?.selectedPerson = newPerson
                }
            )

            SelectedPersonView(
                selectedPersonPublisher: viewModel.$selectedPerson.eraseToAnyPublisher()
            )
        }
        .padding()
    }
}

class ButtonViewModel: ObservableObject {
    @Published var selectedPerson: Person = Person(firstName: "", surname: "")
    private var cancellables = Set<AnyCancellable>()
    private let updateAction: (Person) -> Void

    init(
        selectedPersonPublisher: AnyPublisher<Person, Never>,
        updateAction: @escaping (Person) -> Void
    ) {
        self.updateAction = updateAction
        // 订阅父层数据变化,同步本地状态
        selectedPersonPublisher
            .assign(to: \.selectedPerson, on: self)
            .store(in: &cancellables)
    }

    func toggleSelectedPerson() {
        let newPerson = selectedPerson.firstName.isEmpty ?
            Person(firstName: "Boris", surname: "Johnson") :
            Person(firstName: "", surname: "")
        // 通过闭包通知父层修改
        updateAction(newPerson)
    }
}

struct ButtonView: View {
    @ObservedObject var viewModel: ButtonViewModel

    init(
        selectedPersonPublisher: AnyPublisher<Person, Never>,
        updateSelectedPerson: @escaping (Person) -> Void
    ) {
        self._viewModel = ObservedObject(
            wrappedValue: ButtonViewModel(
                selectedPersonPublisher: selectedPersonPublisher,
                updateAction: updateSelectedPerson
            )
        )
    }

    var body: some View {
        Button(action: viewModel.toggleSelectedPerson) {
            Text("Press to select person")
        }
    }
}

class SelectedPersonViewModel: ObservableObject {
    @Published var selectedPerson: Person = Person(firstName: "", surname: "")
    private var cancellables = Set<AnyCancellable>()

    init(selectedPersonPublisher: AnyPublisher<Person, Never>) {
        selectedPersonPublisher
            .assign(to: \.selectedPerson, on: self)
            .store(in: &cancellables)
    }
}

struct SelectedPersonView: View {
    @ObservedObject var viewModel: SelectedPersonViewModel

    init(selectedPersonPublisher: AnyPublisher<Person, Never>) {
        self._viewModel = ObservedObject(
            wrappedValue: SelectedPersonViewModel(selectedPersonPublisher: selectedPersonPublisher)
        )
    }

    var body: some View {
        if viewModel.selectedPerson.firstName.isEmpty {
            Text("No person selected yet")
        } else {
            Text("Person \(viewModel.selectedPerson.firstName) selected!")
        }
    }
}

struct Person {
    let firstName: String
    let surname: String
}

let view = InitialView()
PlaygroundPage.current.setLiveView(view)

适配你的选项配置场景

针对你补充的多选项配置场景,不需要把所有逻辑合并到单一视图模型,按如下逻辑改造即可:

  1. 父层OptionViewModel的@Published var selectedOptions作为唯一数据源
  2. OptionValueViewModel初始化时接收:
    • $selectedOptions.eraseToAnyPublisher():用于订阅全局选中状态变化
    • 修改闭包:( (inout [Int:[Int]]) -> Void ),用于子层需要修改选中状态时,通知父层执行实际修改
  3. OptionValueViewModel内部可以通过Combine处理订阅到的selectedOptions数据流,计算得到当前选项的isOptionSelected状态,完全不需要持有全局selectedOptions的副本

改造后的OptionValueViewModel核心代码示例:

class OptionValueViewModel: ObservableObject {
    @Published var isOptionSelected: Bool = false
    private var cancellables = Set<AnyCancellable>()
    private let optionID: Int
    private let valueID: Int
    private let updateAction: (inout [Int:[Int]]) -> Void

    init(
        optionID: Int,
        valueID: Int,
        selectedOptionsPublisher: AnyPublisher<[Int:[Int]], Never>,
        updateAction: @escaping (inout [Int:[Int]]) -> Void
    ) {
        self.optionID = optionID
        self.valueID = valueID
        self.updateAction = updateAction
        
        // 直接在VM层用Combine处理数据流,计算专属视图状态
        selectedOptionsPublisher
            .map { selected in
                selected[optionID]?.contains(valueID) ?? false
            }
            .assign(to: \.isOptionSelected, on: self)
            .store(in: &cancellables)
    }

    func removeOption() {
        updateAction { [weak self] selected in
            guard let self else { return }
            selected[self.optionID]?.removeAll { $0 == self.valueID }
        }
    }

    func addOption() {
        updateAction { [weak self] selected in
            guard let self else { return }
            if selected[self.optionID] == nil {
                selected[self.optionID] = []
            }
            selected[self.optionID]?.append(self.valueID)
        }
    }
}

父层OptionView调用时传递对应参数即可:

ForEach(section.values) { value in
    OptionValueView(
        optionID: section.id,
        valueID: value.id,
        selectedOptionsPublisher: viewModel.$selectedOptions.eraseToAnyPublisher(),
        updateAction: { [weak viewModel] transform in
            guard let viewModel else { return }
            transform(&viewModel.selectedOptions)
        }
    )
}

该方案完全符合MVVM职责划分:所有业务逻辑、数据流处理都在视图模型层完成,视图层仅负责渲染和触发用户动作,没有多余逻辑下沉。


内容的提问来源于stack exchange,提问作者Aecasorg

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.07 11:06:02