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

SwiftUI中多子视图与子ViewModel的单一数据源实现方案

SwiftUI多子视图多ViewModel场景下的单一数据源实现方案

我正在开发一个SwiftUI项目,其中包含多个各自关联ViewModel的子视图,希望实现「单一数据源(Single Source of Truth)」的数据管理方案,目标是避免手动同步子视图与父视图间的数据,确保一处修改能自动同步到所有地方。

我曾考虑使用EnvironmentObject来实现,但担心它会导致视图间的紧耦合,以及给测试和UI预览带来困难。

请问在这类多子视图、多子ViewModel的场景下,实现单一数据源有什么推荐方案或最佳实践?我希望保持代码库的整洁可维护性,同时确保数据变更能在应用中无缝传播。

我欢迎任何相关见解、代码示例或设计模式,帮助我维持高效整洁的代码库并实现数据的无缝同步。

感谢您的指导!


示例模拟项目代码

FormView

@MainActor class FormViewModel: ObservableObject {
    var firstStepData = FirstStepDataModel()
    var secondStepData = SecondStepDataModel()
}

struct FormView: View {
    @StateObject var vm = FormViewModel()
    var body: some View {
        VStack {
            // switch between steps and load data back from model.
            FirstStepView(vm: .init(data: vm.firstStepData)) { data in
                vm.firstStepData = data
            }
        }
    }
}

FirstStepView

struct FirstStepDataModel: Codable {
    var personalData = PersonalInformationsDataModel()
    var contactData = ContactInformationsDataModel()
}

@MainActor class FirstStepViewModel: ObservableObject {
    @Published var data: FirstStepDataModel
    let personalVM: PersonalInformationsViewModel
    let contactVM: ContactInformationsViewModel
    private var subscriptions: Set<AnyCancellable> = []
    init(data: FirstStepDataModel) {
        self._data = Published(initialValue: data)
        self.personalVM = PersonalInformationsViewModel(data: data.personalData)
        self.contactVM = ContactInformationsViewModel(data: data.contactData)
        syncSubVMs()
    }

    func syncSubVMs() {
        personalVM.$data
            .receive(on: DispatchQueue.main)
            .sink { [weak self] data in
                self?.data.personalData = data
            }
            .store(in: &subscriptions)
        contactVM.$data
            .receive(on: DispatchQueue.main)
            .sink { [weak self] data in
                self?.data.contactData = data
            }
            .store(in: &subscriptions)
    }
}

struct FirstStepView: View {
    @ObservedObject var vm: FirstStepViewModel
    let nextStep: (FirstStepDataModel) -> Void
    var body: some View {
        VStack {
            PersonalInformationsView(vm: vm.personalVM)
            ContactInformationsView(vm: vm.contactVM)
            Button("Next Step") {
                // go next step
                // send data model to service
                // put step data to FormViewModel's step data.
                nextStep(vm.data)
            }
        }
    }
}

通用视图(可复用)

struct PersonalInformationsDataModel: Codable {
    var firstName: String = ""
    var lastName: String = ""
}

@MainActor final class PersonalInformationsViewModel: ObservableObject {
    @Published var data: PersonalInformationsDataModel
    init(data: PersonalInformationsDataModel) {
        self._data = Published(initialValue: data)
    }
    // Validations...
    // Network requests...
}

struct PersonalInformationsView: View {
    @ObservedObject var vm: PersonalInformationsViewModel

    var body: some View {
        VStack {
            TextField("First Name", text: $vm.data.firstName)
            TextField("Last Name", text: $vm.data.lastName)
        }
    }
}

struct ContactInformationsDataModel: Codable {
    var phoneNumber: String = ""
    var faxNumber: String = ""
}

@MainActor final class ContactInformationsViewModel: ObservableObject {
    @Published var data: ContactInformationsDataModel
    init(data: ContactInformationsDataModel) {
        self._data = Published(initialValue: data)
    }
    // Validations...
    // Network requests...
}

struct ContactInformationsView: View {
    @ObservedObject var vm: ContactInformationsViewModel
    var body: some View {
        VStack {
            TextField("Phone Number", text: $vm.data.phoneNumber)
            TextField("Fax Number", text: $vm.data.faxNumber)
        }
    }
}

我尝试过从父视图通过绑定传递所有数据,但遇到了UI更新问题,同时将ViewModel中的变量绑定回视图也颇具挑战。


推荐方案与最佳实践

方案1:使用引用类型数据模型(Class替代Struct)

当前代码中数据模型都是值类型struct,传递时会产生副本,导致各ViewModel持有独立数据,需要手动同步。将数据模型改为引用类型class,所有ViewModel共享同一实例,自然实现单一数据源:

修改数据模型为Class

class PersonalInformationsDataModel: Codable, ObservableObject {
    @Published var firstName: String = ""
    @Published var lastName: String = ""
    
    // 实现Codable协议
    enum CodingKeys: String, CodingKey {
        case firstName, lastName
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(firstName, forKey: .firstName)
        try container.encode(lastName, forKey: .lastName)
    }
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        firstName = try container.decode(String.self, forKey: .firstName)
        lastName = try container.decode(String.self, forKey: .lastName)
    }
    
    // 默认初始化器
    init() {}
}

// 同理修改其他数据模型
class FirstStepDataModel: Codable, ObservableObject {
    @Published var personalData = PersonalInformationsDataModel()
    @Published var contactData = ContactInformationsDataModel()
    
    // 实现Codable协议...
    init() {}
}

简化ViewModel同步逻辑

因为数据是引用类型,子ViewModel直接持有父模型的引用,无需订阅同步:

@MainActor class FirstStepViewModel: ObservableObject {
    let data: FirstStepDataModel
    let personalVM: PersonalInformationsViewModel
    let contactVM: ContactInformationsViewModel
    
    init(data: FirstStepDataModel) {
        self.data = data
        // 直接传入引用而非副本
        self.personalVM = PersonalInformationsViewModel(data: data.personalData)
        self.contactVM = ContactInformationsViewModel(data: data.contactData)
        // 无需syncSubVMs方法,数据自动同步
    }
}

@MainActor final class PersonalInformationsViewModel: ObservableObject {
    let data: PersonalInformationsDataModel
    
    init(data: PersonalInformationsDataModel) {
        self.data = data
    }
    
    // 验证、网络请求逻辑保持不变
}

视图层修改

视图直接绑定到数据模型的@Published属性,UI自动更新:

struct PersonalInformationsView: View {
    @ObservedObject var data: PersonalInformationsDataModel

    var body: some View {
        VStack {
            TextField("First Name", text: $data.firstName)
            TextField("Last Name", text: $data.lastName)
        }
    }
}

// 修改FirstStepView调用逻辑
struct FirstStepView: View {
    let data: FirstStepDataModel
    let nextStep: () -> Void
    
    var body: some View {
        VStack {
            PersonalInformationsView(data: data.personalData)
            ContactInformationsView(data: data.contactData)
            Button("Next Step") {
                nextStep()
            }
        }
    }
}

// FormView中直接传递引用
struct FormView: View {
    @StateObject var vm = FormViewModel()
    var body: some View {
        VStack {
            FirstStepView(data: vm.firstStepData) {
                // 无需手动赋值,数据已自动同步到vm.firstStepData
            }
        }
    }
}

方案2:使用Combine实现双向绑定(保留值类型)

如果坚持使用值类型数据模型,可通过Combine将子ViewModel的@Published数据与父模型双向绑定,避免手动订阅的繁琐:

双向绑定扩展工具

extension Publisher where Output: Equatable, Failure == Never {
    func bind<T>(to subject: Published<T>.Publisher, on object: ObservableObject) -> AnyCancellable where T == Output {
        self.combineLatest(subject)
            .filter { $0 != $1 }
            .map(\.0)
            .assign(to: subject.projectedValue, on: object)
    }
}

修改FirstStepViewModel同步逻辑

@MainActor class FirstStepViewModel: ObservableObject {
    @Published var data: FirstStepDataModel
    let personalVM: PersonalInformationsViewModel
    let contactVM: ContactInformationsViewModel
    private var subscriptions: Set<AnyCancellable> = []
    
    init(data: FirstStepDataModel) {
        self._data = Published(initialValue: data)
        self.personalVM = PersonalInformationsViewModel(data: data.personalData)
        self.contactVM = ContactInformationsViewModel(data: data.contactData)
        
        // 双向绑定:子VM数据变更同步到父模型,父模型变更同步到子VM
        personalVM.$data.bind(to: $data.personalData, on: self)
        contactVM.$data.bind(to: $data.contactData, on: self)
        
        $data.personalData
            .filter { $0 != self.personalVM.data }
            .assign(to: &self.personalVM.$data)
        
        $data.contactData
            .filter { $0 != self.contactVM.data }
            .assign(to: &self.contactVM.$data)
    }
}

方案3:状态容器(State Container)模式

创建全局或局部的状态容器,统一管理所有表单数据,各ViewModel通过依赖注入获取容器实例,实现单一数据源:

定义状态容器

@MainActor class FormStateContainer: ObservableObject {
    @Published var firstStep = FirstStepDataModel()
    @Published var secondStep = SecondStepDataModel()
    
    // 全局表单操作逻辑
    func submitForm() async throws {
        // 提交所有步骤数据到服务器
    }
}

依赖注入到ViewModel

@MainActor class FirstStepViewModel: ObservableObject {
    private let state: FormStateContainer
    var firstStepData: FirstStepDataModel {
        get { state.firstStep }
        set { state.firstStep = newValue }
    }
    
    let personalVM: PersonalInformationsViewModel
    let contactVM: ContactInformationsViewModel
    private var subscriptions: Set<AnyCancellable> = []
    
    init(state: FormStateContainer) {
        self.state = state
        self.personalVM = PersonalInformationsViewModel(data: state.firstStep.personalData)
        self.contactVM = ContactInformationsViewModel(data: state.firstStep.contactData)
        
        // 同步子VM数据到状态容器
        personalVM.$data
            .sink { [weak self] in
                self?.state.firstStep.personalData = $0
            }
            .store(in: &subscriptions)
        
        contactVM.$data
            .sink { [weak self] in
                self?.state.firstStep.contactData = $0
            }
            .store(in: &subscriptions)
    }
}

视图中注入状态容器

struct FormView: View {
    @StateObject private var stateContainer = FormStateContainer()
    var body: some View {
        VStack {
            FirstStepView(vm: FirstStepViewModel(state: stateContainer))
            // 其他步骤视图同样注入stateContainer
        }
    }
}

方案对比与选型建议

  • 引用类型模型:实现最简单,适合中小规模表单,无需额外同步逻辑,但需要处理Codable协议,且模型变更会直接影响所有引用处。
  • Combine双向绑定:保留值类型的语义优势,适合需要严格值不可变性的场景,但需要编写绑定逻辑,代码量略多。
  • 状态容器模式:适合大型复杂表单,统一管理所有状态,便于测试和扩展,通过手动注入避免EnvironmentObject的紧耦合问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 11:54:54