具有复杂 MVVM(存储库 + 嵌套观察对象)的 SwiftUI

SwiftUI with complex MVVM (Repository + Nested ObservedObject)

说明

我仍在学习以最佳方式利用 SwiftUI 模式。但我发现的大多数 SwiftUI MVVM 实现示例都非常简单。他们通常有一个数据库 class,然后是 1-2 个从那里获取数据的视图模型,然后你就有了视图。

在我的应用程序中,我有一个 SQLite 数据库、Firebase 和不同的内容领域。所以我有几个单独的模型-虚拟机-视图路径。在我的应用程序的 Android 等价物中,我使用了这样的模式:

View - ViewModel - Repository - Database

通过这种方式,我可以像存储库 classes 中的所有 SQL 查询一样分离数据库逻辑,并让 VM 仅处理与视图相关的逻辑。所以整个事情看起来像这样:

在 Android 中工作正常,因为我只是将 LiveData 对象传递给视图。但是在 SwiftUI 中尝试这种模式时,我有点碰壁了:

  1. 它不起作用/我不知道如何正确连接
  2. Published对象
  3. “链接”或嵌套 ObservableObject 的想法似乎不受欢迎:

这篇文章关于 Nested Observable Objects in SwiftUI:

I’ve seen this pattern described as “nested observable objects”, and it’s a subtle quirk of SwiftUI and how the Combine ObservableObject protocol works that can be surprising. You can work around this, and get your view updating with some tweaks to the top level object, but I’m not sure that I’d suggest this as a good practice. When you hit this pattern, it’s a good time to step back and look at the bigger picture.

所以似乎有人被推向使用更简单的模式:

View - ViewModel - Database 资源库

中间没有存储库。但这对我来说似乎很烦人,它会使我的视图模型 classes 臃肿,并将 UI/business 代码与 SQL 查询混合。


我的代码

所以这是我的代码的简化版本来演示问题:

存储库:

class SA_Repository: ObservableObject {
    
    @Published var selfAffirmations: [SelfAffirmation]?
    private var dbQueue: DatabaseQueue?
    
    init() {
        do {
            dbQueue = Database.sharedInstance.dbQueue
            fetchSelfAffirmations()
            
            // Etc. other SQL code
        } catch {
            print(error.localizedDescription)
        }
    }
    
    private func fetchSelfAffirmations() {

        let saObservation = ValueObservation.tracking { db in
            try SelfAffirmation.fetchAll(db)
        }
        if let unwrappedDbQueue = dbQueue {
            let _ = saObservation.start(
                in: unwrappedDbQueue,
                scheduling: .immediate,
                onError: {error in print(error.localizedDescription)},
                onChange: {selfAffirmations in
                    print("change in SA table noticed")
                    self.selfAffirmations = selfAffirmations
                })
        }
    }
    
    public func updateSA() {...}
    public func insertSA() {...}
    // Etc.
}

视图模型:

class SA_ViewModel: ObservableObject {
    @ObservedObject private var saRepository  = SA_Repository()
    @Published var selfAffirmations: [SelfAffirmation] = []
    
    init() {
        selfAffirmations = saRepository.selfAffirmations ?? []
    }
    
    public func updateSA() {...}
    public func insertSA() {...}
    
    // + all the Firebase stuff later on
}

查看:

struct SA_View: View {
    @ObservedObject 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)
                                })
                    
            }
        }
    }
}

尝试次数

显然我在这里做的方式是错误的,因为它使用 selfAffirmations = saRepository.selfAffirmations ?? [] 将数据从 repo 克隆到 vm 一次,但是当我从视图中编辑条目时它永远不会更新,只有在应用程序重新启动时才会更新。

我试过 $selfAffirmations = saRepository.$selfAffirmations 只转移绑定。但是 repo 一个是可选的,所以我需要使 vm selfAffirmations 也成为可选的,这意味着在视图代码中处理不必要的逻辑。并且不确定它是否会起作用。

我尝试使用 Combine 手动完成,但这种方式似乎不被推荐并且很脆弱。另外它也没有用:

selfAffirmations = saRepository.selfAffirmations ?? []
        cancellable = saRepository.$selfAffirmations.sink(
            receiveValue: { [weak self] repoSelfAffirmations in
                self?.selfAffirmations = repoSelfAffirmations ?? []
            }
        )

问题

总的来说,我只需要一些方法来将数据从存储库传递到视图,但让虚拟机作为分隔符位于中间。我在 Combine 中读到了 PassthroughSubject,这听起来很合适,但我不确定我是否只是误解了这里的一些概念。

现在我不确定我的架构概念是否是 wrong/unfitting,或者我是否对 Combine 发布者还不够了解,所以无法完成这项工作。

如有任何建议,我们将不胜感激。

从评论中得到一些意见后,我想出了一个干净的方法。

我的问题是了解如何使 属性 或 class 发布其值。因为评论表明像 @ObservedObject 这样的 属性 包装器是 frontend/SwiftUI 唯一的东西,所以我假设所有相关的东西也仅限于此,比如 @Published.

所以我一直在寻找类似 selfAffirmations.makePublisher {...} 的东西,它能让我的 属性 成为可订阅的值发射器。我发现数组自然带有 .publisher 属性,但这个数组似乎只发出一次值,再也不会发出值。

最终我发现 @Published 可以在 没有 @ObservableObject 的情况下使用并且仍然可以正常工作!它将任何 属性 变成已发布的 属性.

所以现在我的设置如下所示:

存储库(使用 GRDB.swift 顺便说一句):

class SA_Repository {
    
    private var dbQueue: DatabaseQueue?
    @Published var selfAffirmations: [SelfAffirmation]?
    // Set of cancellables so they live as long as needed and get deinitialiazed with the class end
    var subscriptions = Array<DatabaseCancellable>()
    
    init() {
        dbQueue = Database.sharedInstance.dbQueue
        fetchSelfAffirmations()
    }
    
    private func fetchSelfAffirmations() {
        // DB code....
    }
}

和视图模型:

class SA_ViewModel: ObservableObject {
    private var saRepository  = SA_Repository()
    @Published var selfAffirmations: [SelfAffirmation] = []
    // Set of cancellables to keep them running
    var subscriptions = Set<AnyCancellable>()
    
    init() {
        saRepository.$selfAffirmations
            .sink{ [weak self] repoSelfAffirmations in
                self?.selfAffirmations = repoSelfAffirmations ?? []
            }
            .store(in: &subscriptions)
    }
}