当 Store 的对象更新时,自动触发 ViewModel ObservableObjects 中的 objectWillChange.send()

When a Store's object is updated, auto-trigger objectWillChange.send() in ViewModel ObservableObjects

对于使用 Combine 和 SwiftUI 的 Store/Factory/ViewModel 模式,我想要一个符合 Store 协议的 class 在指定模型对象更改内部属性时公开发布者。然后任何订阅的 ViewModel 都可以触发 objectWillChange 来显示更改。

(这是必要的,因为通过引用传递的模型对象 中的更改被忽略 ,因此 @Published/ObservableObject 不会为工厂传递自动触发Store-owned 模型。它可以在 Store 和 VM 中调用 objectWillChange,但这会排除任何被动监听的 VM。)

这是一个委托模式,对吧,将@Published/ObservableObject 扩展到按引用传递的对象?通过结合博客、书籍和文档进行梳理并没有引发什么可能是非常标准的想法。

粗暴的工作尝试

我认为如果我在外部公开 VM 的 objectWillChange PassthroughSubject 会很有用,但是 PassthroughSubject.send() 将为模型对象中的每个对象触发。可能很浪费(尽管 ViewModel 只触发它的 objectWillChange 一次)。

Ext+VM republishChanges(of myStore: Store) 上附加一个限制器(例如节流阀、removeDuplicates)似乎并没有限制 .sink 调用,我也没有看到一个明显的方法来重置 PassthroughSubject 和 VM 的接收器之间的需求... 或了解如何将订阅者附加到符合协议的 PassthroughSubject。有什么建议吗?

店面

struct Library {
   var books: // some dictionary
}

class LocalLibraryStore: LibraryStore {
    private(set) var library: Library {
         didSet { publish() }
    }

    var changed = PassthroughSubject<Any,Never>()
    func removeBook() {}
}

protocol LibraryStore: Store { 
    var changed: PassthroughSubject<Any,Never> { get }
    var library: Library { get }
}


protocol Store {
    var changed: PassthroughSubject<Any,Never> { get }
}

extension Store {
    func publish() {
        changed.send(1)
        print("This will fire once.")
    }
}

虚拟机端

class BadgeVM: VM {
    init(store: LibraryStore) {
        self.specificStore = store
        republishChanges(of: jokesStore)
    }

    var objectWillChange = ObservableObjectPublisher() // Exposed {set} for external call
    internal var subscriptions = Set<AnyCancellable>()

    @Published private var specificStore: LibraryStore
    var totalBooks: Int { specificStore.library.books.keys.count }
}

protocol VM: ObservableObject {
    var subscriptions: Set<AnyCancellable> { get set }
    var objectWillChange: ObservableObjectPublisher { get set }
}

extension VM {
    internal func republishChanges(of myStore: Store) {
        myStore.changed
            // .throttle() doesn't silence as hoped
            .sink { [unowned self] _ in
                print("Executed for each object inside the Store's published object.")
                self.objectWillChange.send()
            }
            .store(in: &subscriptions)
    }
}

class OtherVM: VM {
    init(store: LibraryStore) {
        self.specificStore = store
        republishChanges(of: store)
    }

    var objectWillChange = ObservableObjectPublisher() // Exposed {set} for external call
    internal var subscriptions = Set<AnyCancellable>()

    @Published private var specificStore: LibraryStore
    var isBookVeryExpensive: Bool { ... }
    func bookMysteriouslyDisappears() { 
         specificStore.removeBook() 
    }
}

看来你想要的是一种在其内部属性发生变化时发出通知的类型。这听起来很像 ObservableObject 所做的事情。

所以,让你的 Store 协议继承自 ObservableObject:

protocol Store: ObservableObject {}

然后符合 Store 的类型可以决定要通知哪些属性,例如 @Published:

class StringStore: Store {
   @Published var text: String = ""
}

其次,您希望您的视图模型在商店通知他们时自动触发他们的 objectWillChange 发布者。

自动部分可以使用基础 class 来完成 - 而不是使用协议 - 因为它需要存储订阅。您可以保留协议要求,如果您需要:

protocol VM {
   associatedtype S: Store
   var store: S { get }
}

class BaseVM<S: Store>: ObservableObject, VM {
   var c : AnyCancellable? = nil
    
   let store: S
    
   init(store: S) {
      self.store = store

      c = self.store.objectWillChange.sink { [weak self] _ in
         self?.objectWillChange.send()
      }
   }
}

class MainVM: BaseVM<StringStore> {
   // ...
}

这是一个如何使用它的例子:


let stringStore = StringStore();
let mainVm = MainVM(store: stringStore)

// this is conceptually what @ObservedObject does
let c = mainVm.objectWillChange.sink { 
   print("change!") // this will fire after next line
} 

stringStore.text = "new text"

感谢@NewDev 指出子类化是一条更聪明的路线。

如果你想嵌套 ObservableObjects 或让 ObservableObject re-publish 改变传递给它的对象中的对象,这种方法比我的问题使用更少的代码。

在搜索使用 属性 包装器进一步简化(获取父 objectWillChange 并进一步简化它)时,我注意到此线程中的类似方法:。这仅在使用可变参数时有所不同。

定义 VM 和 Store/Repo 类

import Foundation
import Combine

class Repo: ObservableObject {
    func publish() {
        objectWillChange.send()
    }
}

class VM: ObservableObject {
    private var repoSubscriptions = Set<AnyCancellable>()

    init(subscribe repos: Repo...) {
        repos.forEach { repo in
            repo.objectWillChange
                .receive(on: DispatchQueue.main) // Optional
                .sink(receiveValue: { [weak self] _ in
                    self?.objectWillChange.send()
                })
                .store(in: &repoSubscriptions)
        }
    }
}

示例实现

  • 回购:将 didSet { publish() } 添加到模型对象
  • VM:super.init() 接受任意数量的 repos 以重新发布
import Foundation

class UserDirectoriesRepo: Repo, DirectoriesRepository {
    init(persistence: Persistence) {
        self.userDirs = persistence.loadDirectories()
        self.persistence = persistence
        super.init()
        restoreBookmarksAccess()
    }

    private var userDirs: UserDirectories {
        didSet { publish() }
    }

    var someExposedSliceOfTheModel: [RootDirectory] {
        userDirs.rootDirectories.filter { [=11=].restoredURL != nil }
    }

    ...
}
import Foundation

class FileStructureVM: VM {
    init(directoriesRepo: DirectoriesRepository) {
        self.repo = directoriesRepo
        super.init(subscribe: directoriesRepo)
    }
    
    @Published // No longer necessary
    private var repo: DirectoriesRepository
    
    var rootDirectories: [RootDirectory] {
        repo.rootDirectories.sorted ...
    }

    ...
}