Search code examples
swiftcore-datacombinepublisher

Why does this Swift Publisher never fire?


I'm really confused why this Publisher is not firing, I would very much appreciate someone experienced shedding light on what is going on here.

I'm trying to build a service that exposes a Combine interface for accessing some data in Core Data.

I have the following code which should be a relatively complete example to demonstrate my issue (you'll have to do a bit of extra set to get things compiling but if you are familiar with CD and UIKit you shouldn't have an issue) :


/// 1. Core Data Stack

import CoreData

struct CoreDataStack {
    
    let container: NSPersistentCloudKitContainer

    init(
        name: String = "TasksPrototype",
        inMemory: Bool = false
    ) {
        container = NSPersistentCloudKitContainer(name: name)
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        container.viewContext.automaticallyMergesChangesFromParent = true
    }
}

/// 2. Task Data Models (CoreData + UI model)

@objc(TaskEntity)
public class TaskEntity: NSManagedObject {

}

extension TaskEntity {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<TaskEntity> {
        return NSFetchRequest<TaskEntity>(entityName: "TaskEntity")
    }

    @NSManaged public var id: UUID?
    @NSManaged public var title: String?
    @NSManaged public var creationDate: Date?

}

extension TaskEntity : Identifiable {

}


struct Task: Equatable {
    var id: UUID
    var creationDate: Date
    var title: String
    
    init(
        id: UUID = UUID(),
        creationDate: Date = Date(),
        title: String = "Untitled"
    ) {
        self.id = id
        self.creationDate = creationDate
        self.title = title
    }
    
    init?(
        entity: TaskEntity
    ) {
        guard let id = entity.id,
              let creationDate = entity.creationDate,
              let title = entity.title else { fatalError() }
        
        self.id = id
        self.creationDate = creationDate
        self.title = title
    }
}

/// 3. Service

struct TaskService {
    
    private let coreDataStack: CoreDataStack
    let monitorPublisher: ManagedObjectChangesPublisher<TaskEntity>

    init(
        coreDataStack: CoreDataStack
    ) {
        self.coreDataStack = coreDataStack
        let req = TaskEntity.fetchRequest()
        req.sortDescriptors = []
        self.monitorPublisher = coreDataStack.container.viewContext.changesPublisher(for: req)
    }

    func monitor() -> AnyPublisher<[Task], Error> {
        return monitorPublisher
            .reduce([]) { (accum: [TaskEntity], diff: CollectionDifference<TaskEntity>) -> [TaskEntity] in
                return accum.applying(diff) ?? []
            }
            .compactMap { $0.compactMap(Task.init(entity:)) }
            .eraseToAnyPublisher()
    }

/// 4. ManagedObjectChangesPublisher


/// See https://gist.githubusercontent.com/andreyz/757ec98b5e567cddd5ff55e1fd2c1e19/raw/c2e4638861bb39b77dcca72a29f29a3c2bacc559/ManagedObjectChangesPublisher.swift


/// 5. View Controller

class ViewController: UIViewController {

    private let taskService = TaskService(coreDataStack: CoreDataStack())
    private var cancellables: [AnyCancellable] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .white
        
        taskService.monitor()
            .sink(receiveCompletion: { _ in }) { tasks in
                print(tasks)
            }.store(in: &cancellables)
    }

}

I can set a breakpoint in TaskService and see that my reduce is getting called, but no operation after that is getting called, and in my VC, the sink isn't called.

It's not an issue of having no items, because even when I return some dummy items from TaskService, I'm not doing any work in ViewController.

Something seems to be going wrong subscribing to this Publisher, but I have no idea what it is.

Thanks in advance for any help.

I've tried setting print() debug operations, setting breakpoints, but I'm still really lost on why the Publisher is not firing.


Solution

  • Finally figured out why my Publisher was never firing.

    I was using the reduce operator, which according to the documentation produces a value when the upstream publisher finishes.

    Well, the upstream publisher never finishes in this case, because it's a Notification publisher. The operator I actually wanted was scan - which passes along each accumulated value as it receives it, and doesn't wait for the upstream publisher to complete.