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

SwiftUI复杂MVVM架构(Repository+嵌套ObservedObject)实现难题咨询

我完全理解你的困扰——在SwiftUI里照搬Android的MVVM+Repository架构确实会遇到不少适配问题,尤其是嵌套ObservableObject的限制和Combine订阅的坑。下面是针对你的需求的可行解决方案,既能保留ViewModel和Repository的分层,又能实现实时的数据更新:

核心问题拆解

你当前的问题主要出在两个地方:

  1. 嵌套ObservableObject不被SwiftUI自动追踪:ViewModel里用@ObservedObject持有Repository的方式无效,因为SwiftUI只会监听视图直接绑定的ObservableObject的@Published属性,不会递归监听嵌套对象的变化。
  2. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.30 12:32:48