SwiftUI MVVM模式下如何在视图模型间传递@Published值并支持修改
问题根因
你原代码的核心问题是通过Published(wrappedValue: viewModel.selectedPerson)创建了新的@Published容器,和父视图模型的属性属于完全独立的两个存储单元,修改其中一个不会触发另一个的更新,自然无法同步。
你之前尝试的@Binding、@EnvironmentObject方案不符合预期的原因是:前者是面向视图层的双向绑定载体,无法直接在视图模型层接入Combine数据流处理;后者需要将逻辑下沉到视图层,不符合MVVM职责划分要求。
规范实现方案
核心思路是保持单一数据源,仅在父子视图模型之间传递数据流通道和修改权限:
- 父视图模型的
@Published属性作为唯一可信数据源 - 子视图模型初始化时接收两个入参:
- 父属性的
Publisher(即父@Published的投影值$xxx),用于订阅数据变化,接入Combine处理 - 父属性的修改闭包,子视图模型需要修改数据时通过闭包通知父层更新,避免直接持有数据副本
- 父属性的
- 子视图模型内部可以基于接收的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)
适配你的选项配置场景
针对你补充的多选项配置场景,不需要把所有逻辑合并到单一视图模型,按如下逻辑改造即可:
- 父层
OptionViewModel的@Published var selectedOptions作为唯一数据源 OptionValueViewModel初始化时接收:$selectedOptions.eraseToAnyPublisher():用于订阅全局选中状态变化- 修改闭包:
( (inout [Int:[Int]]) -> Void ),用于子层需要修改选中状态时,通知父层执行实际修改
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
相关产品推荐
相关产品推荐

