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