Skip to content

Instantly share code, notes, and snippets.

@wingovers
Last active April 22, 2024 21:22
Show Gist options
  • Save wingovers/5d6e27090e747f5d504cf9405e0bbbb4 to your computer and use it in GitHub Desktop.
Save wingovers/5d6e27090e747f5d504cf9405e0bbbb4 to your computer and use it in GitHub Desktop.
Nesting ObservableObjects / Observing Repositories for SwiftUI

Nesting ObservableObjects / Observing Repositories for SwiftUI View Models

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.

Define VM and Store/Repo Superclasses

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)
        }
    }
}

Example Implementation

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 }
    }

    ...
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment