Reinitialize Combine Publishers When User Changes Global Object

Viewed 185

I have a Core Data publisher that is working great so far. I have a Workspace entity and a Project entity. I use the following publisher to get all the projects for a given workspace:

class ProjectModel: ObservableObject {
  @Published var projects = [Project]()
  private var cancellableSet: Set<AnyCancellable> = []

  init(){
    CoreDataPublisher(request: Project.getAllProjects(), context: PersistenceController.shared.container.viewContext)
      .sink(
        receiveCompletion: { print($0) },
        receiveValue: { [weak self] items in
          self?.projects = items
        })
      .store(in: &cancellableSet)
  }
}

The fetch request getAllProjects() is in a Core Data entity extension here where the NSPredicate filters based on a Workspace object set in the UI.

//Core Data Entity Extension
extension Project{
  @nonobjc public class func getAllProjects() -> NSFetchRequest<Project> {
    let workspace = AppState.shared.workspace as Workspace //<-- The user can change this workspace
  
    let request = NSFetchRequest<Project>(entityName: "\(Self.self)")
    request.sortDescriptors = [NSSortDescriptor(keyPath: \Project.name, ascending: true)]
    request.predicate = NSPredicate(format: "workspace = %@", workspace)
    return request
  }
}

This Workspace object is in a global state class:

class AppState: ObservableObject{
  static let shared = AppState()
  @Published var workspace: Workspace!

  init(){
    //Setup the workspace for the first time
  }
}

I can successfully receive data from my publisher and I can successfully change the global Workspace in the UI. The problem is that after changing the Workspace, the publisher still points to the old Workspace originally set when the fetch request was created.

How do I prompt the ProjectModel to reinitialize in order to renew the publisher's state when the AppState's workspace is changed?

2 Answers

Typically the way to accomplish this is to use the flatMap operator on your publisher. flatMap lets you "create a new publisher based on this received value, and then use that publisher's output as the overall publisher chain's output".

It would look something like this:

AppState.shared.$workspace.flatMap { workspace in
    let request = NSFetchRequest<Project>(entityName: "\(Project.self)")
    request.sortDescriptors = [NSSortDescriptor(keyPath: \Project.name, ascending: true)]
    request.predicate = NSPredicate(format: "workspace = %@", workspace)
    return CoreDataPublisher(request: request, context: PersistenceController.shared.container.viewContext)
}

This gives you a new publisher that:

  • when the .workspace property changes on your app state, constructs a new CoreDataPublisher based off whatever the new Workspace value is
  • uses that CoreDataPublisher as the source of values for the overall publisher stream

In your ProjectModel, I'd switch from the Set of AnyCancellable to a specific one so that you can cancel it:

var cdPublisherCancellable : AnyCancellable?

I'd move the setup of this publisher out of init, since you'll need to call it again:

func setupPublisher() {
  cdPublisherCancellable?.cancel()
  cdPublisherCancellable = CoreDataPublisher(request:)...
}

Then, since workspace is a published property on your shared AppState, I'd set up another publisher link to watch it:

var workspaceCancellable : AnyCancellable?
init() {
  workspaceCancellable = AppState.shared.$workspace.sink { workspace in
    setupPublisher()
  }
}
Related