SwiftUI复杂MVVM架构(Repository+嵌套ObservedObject)实现难题咨询
我完全理解你的困扰——在SwiftUI里照搬Android的MVVM+Repository架构确实会遇到不少适配问题,尤其是嵌套ObservableObject的限制和Combine订阅的坑。下面是针对你的需求的可行解决方案,既能保留ViewModel和Repository的分层,又能实现实时的数据更新:
核心问题拆解
你当前的问题主要出在两个地方:
- 嵌套
ObservableObject不被SwiftUI自动追踪:ViewModel里用@ObservedObject持有Repository的方式无效,因为SwiftUI只会监听视图直接绑定的ObservableObject的@Published属性,不会递归监听嵌套对象的变化。 - Combine订阅被提前释放:你之前尝试的
sink订阅失效,是因为cancellable是局部变量,ViewModel初始化完成后就被销毁,导致订阅被取消,无法接收后续的数据更新。
解决方案1:修复Combine订阅(保留现有Repository结构)
如果想继续让Repository作为ObservableObject,只需要修正ViewModel的订阅逻辑,确保订阅被持久化,同时简化可选值的处理:
import Combine class SA_ViewModel: ObservableObject { private let saRepository = SA_Repository() @Published var selfAffirmations: [SelfAffirmation] = [] // 必须用Set存储订阅,避免订阅被提前释放 private var cancellables = Set<AnyCancellable>() init() { // 订阅Repository的selfAffirmations变化,自动同步到ViewModel的属性 saRepository.$selfAffirmations .replaceNil(with: []) // 把可选数组转为非空,视图层无需处理可选值 .assign(to: \.selfAffirmations, on: self) .store(in: &cancellables) // 如果需要额外的逻辑处理,也可以用sink: // saRepository.$selfAffirmations // .sink(receiveValue: { [weak self] repoSA in // guard let self = self else { return } // self.selfAffirmations = repoSA ?? [] // // 这里可以加一些UI相关的逻辑,比如显示Toast // }) // .store(in: &cancellables) } // 代理Repository的操作方法,ViewModel只处理UI相关逻辑 public func updateSA(id: Int, newContent: String) { saRepository.updateSA(id: id, newContent: newContent) } public func insertSA(_ sa: SelfAffirmation) { saRepository.insertSA(sa) } // Firebase相关逻辑放在这里,比如调用Firebase API后通知Repository更新 }
关键调整点:
- 用
Set<AnyCancellable>存储订阅对象,确保订阅在ViewModel生命周期内持续有效 - 使用
replaceNil操作符统一处理可选值,避免视图层写大量的if let assign(to:on:)是简洁的绑定方式,适合直接同步数据的场景
解决方案2:让Repository返回明确的Publisher(更推荐的架构实践)
如果想让架构更符合SwiftUI/Combine的设计理念,建议把Repository从ObservableObject改为提供明确的Publisher,这样Repository的职责更单一(只处理数据逻辑,不关心UI层的观察):
调整后的Repository层:
import Combine class SA_Repository { private var dbQueue: DatabaseQueue? // 用CurrentValueSubject存储当前数据,初始值为空数组 private let selfAffirmationsSubject = CurrentValueSubject<[SelfAffirmation], Error>([]) // 对外暴露只读的Publisher,隐藏内部的Subject实现 var selfAffirmationsPublisher: AnyPublisher<[SelfAffirmation], Error> { selfAffirmationsSubject.eraseToAnyPublisher() } init() { do { dbQueue = Database.sharedInstance.dbQueue fetchSelfAffirmations() } catch { selfAffirmationsSubject.send(completion: .failure(error)) } } private func fetchSelfAffirmations() { let saObservation = ValueObservation.tracking { db in try SelfAffirmation.fetchAll(db) } guard let unwrappedDbQueue = dbQueue else { return } let _ = saObservation.start( in: unwrappedDbQueue, scheduling: .immediate, onError: { [weak self] error in self?.selfAffirmationsSubject.send(completion: .failure(error)) }, onChange: { [weak self] selfAffirmations in print("change in SA table noticed") self?.selfAffirmationsSubject.send(selfAffirmations) }) } public func updateSA(id: Int, newContent: String) { do { guard let dbQueue = dbQueue else { return } try dbQueue.write { db in let sa = try SelfAffirmation.fetchOne(db, key: id) sa?.content = newContent try sa?.save(db) } // SQLite的ValueObservation会自动触发onChange,无需手动发送数据 } catch { selfAffirmationsSubject.send(completion: .failure(error)) } } public func insertSA(_ sa: SelfAffirmation) { do { guard let dbQueue = dbQueue else { return } try dbQueue.write { db in try sa.save(db) } } catch { selfAffirmationsSubject.send(completion: .failure(error)) } } }
对应的ViewModel层:
import Combine class SA_ViewModel: ObservableObject { private let saRepository = SA_Repository() @Published var selfAffirmations: [SelfAffirmation] = [] private var cancellables = Set<AnyCancellable>() init() { saRepository.selfAffirmationsPublisher // 统一处理错误,避免错误传递到视图层 .catch { error -> AnyPublisher<[SelfAffirmation], Never> in print("Failed to fetch self affirmations: \(error.localizedDescription)") return Just([]).eraseToAnyPublisher() } .assign(to: \.selfAffirmations, on: self) .store(in: &cancellables) } // 代理操作方法,ViewModel专注于UI逻辑 public func updateSA(id: Int, newContent: String) { saRepository.updateSA(id: id, newContent: newContent) } public func insertSA(_ sa: SelfAffirmation) { saRepository.insertSA(sa) } // Firebase逻辑在这里实现,比如调用Firebase API后,通知Repository同步数据 }
这个方案的优势:
- 职责分离更清晰:Repository只负责数据的获取和操作,不依赖UI层的
ObservableObject协议 - 错误处理更灵活:可以在ViewModel层统一处理数据源的错误,避免影响视图
- 扩展性更强:后续添加Firebase等其他数据源时,只需要在Repository层扩展,ViewModel无需大幅修改
视图层的小优化
最后,建议把视图里的@ObservedObject改为@StateObject(SwiftUI 2.0+支持),这样ViewModel会在视图的生命周期内保持唯一实例,避免不必要的重建:
struct SA_View: View { @StateObject private var saViewModel = SA_ViewModel() var body: some View { NavigationView { List(saViewModel.selfAffirmations, id: \.id) { selfAffirmation in SA_ListitemView(content: selfAffirmation.content, editedValueCallback: { newString in saViewModel.updateSA(id: selfAffirmation.id, newContent: newString) }) } } } }
额外建议
对于多模块和多数据源的场景,你可以进一步拆分Repository:比如创建SQLiteSADataSource和FirebaseSADataSource分别处理不同的数据来源,然后用一个SA_Repository来协调这两个数据源,ViewModel只和统一的SA_Repository交互,这样能保持ViewModel的简洁,同时降低模块间的耦合。
内容的提问来源于stack exchange,提问作者Big_Chair
相关产品推荐
相关产品推荐

