Situation
A simple app has a few repository objects that own the in-memory instance of specific models and organize persistence appropriately. Its instantiated by a DI container and passed to view models.
Observation
SwiftUI views do not diff and update upon changes within the repo's model. The view model does not fire its objectWillChange publisher – even though the repository is marked as @Published.
Cause
The model data are passed by reference, not as values, so changes within the object do not alter its identity. The @Published wrapper is not designed to respond to such change.
Potential Solutions
"Re-publishing" referenced objects or nesting ObservableObjects can be accomplished in several ways. The following approach uses two superclasses to attempt to minimize calling code.
Wish List
A @ReferencePublished
property wrapper could simplify this further if I knew how to access the parent class's objectWillChange publisher.
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) // Extra safety
.sink(receiveValue: { [weak self] _ in
self?.objectWillChange.send()
})
.store(in: &repoSubscriptions)
}
}
}
Below, a view model (subclass of VM
) subscribes to any number of repositories in super.init(...)
.
import Foundation
class FileStructureVM: VM {
init(directoriesRepo: DirectoriesRepository, preferencesRepo: PreferencesRepository) {
self.dirsRepo = directoriesRepo
self.prefsRepo = preferencesRepo
super.init(subscribe: directoriesRepo, preferencesRepo)
}
@Published private var dirsRepo: DirectoriesRepository
@Published private var prefsRepo: PreferencesRepository
var rootDirectories: [RootDirectory] {
repo.rootDirectories.sorted ...
}
...
}
The repository subclass of Repo
also conforms to a protocol for composition. It adds didSet { publish() }
to any owned objects it wants to publish.
import Foundation
protocol DirectoriesRepository: Repo {
var someExposedSliceOfTheModel: [RootDirectory] { get }
}
class UserDirectoriesRepo: Repo, DirectoriesRepository {
init(persistence: Persistence) {
self.userDirs = persistence.loadDirectories()
self.persistence = persistence
super.init()
restoreSecurityScopedBookmarksAccess()
}
private var userDirs: UserDirectories {
didSet { publish() }
}
var someExposedSliceOfTheModel: [RootDirectory] {
userDirs.rootDirectories.filter { $0.restoredURL != nil }
}
...
}