For a Store/Factory/ViewModel pattern using Combine and SwiftUI, I'd like a Store protocol-conforming class to expose a publisher for when specified model object(s) change internal properties. Any subscribed ViewModels can then trigger objectWillChange to display the changes.
(This is necessary because changes are ignored inside a model object that is passed by reference, so @Published/ObservableObject won't auto-fire for Factory-passed Store-owned models. It works to call objectWillChange in the Store and the VM, but that leaves out any passively listening VMs.)
That's a delegate pattern, right, extending @Published/ObservableObject to passed-by-reference objects? Combing through combine blogs, books, and docs hasn't triggered an idea to what's probably a pretty standard thing.
Crudely Working Attempt
I thought PassthroughSubject<Any,Never> would be useful if I exposed a VM's objectWillChange externally, but PassthroughSubject.send() will fire for every object within the model object. Wasteful maybe (although the ViewModel only fires its objectWillChange once).
Attaching a limiter (e.g., throttle, removeDuplicates) on Ext+VM republishChanges(of myStore: Store)
didn't seem to limit the .sink calls, nor do I see an obvious way to reset the demand between the PassthroughSubject and the VM's sink... or understand how to attach a Subscriber to a PassthroughSubject that complies with the Protcols. Any suggestions?
Store-Side
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.")
}
}
VM-Side
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()
}
}