Skip to content

Instantly share code, notes, and snippets.

@mjm
Created November 3, 2019 20:41
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save mjm/750b20e1dfd5b1abc82b8295b54b3c74 to your computer and use it in GitHub Desktop.
Save mjm/750b20e1dfd5b1abc82b8295b54b3c74 to your computer and use it in GitHub Desktop.
Observe changes to a Core Data fetch request with Combine
import Combine
import CoreData
extension NSManagedObjectContext {
func changesPublisher<Object: NSManagedObject>(for fetchRequest: NSFetchRequest<Object>)
-> ManagedObjectChangesPublisher<Object>
{
ManagedObjectChangesPublisher(fetchRequest: fetchRequest, context: self)
}
}
struct ManagedObjectChangesPublisher<Object: NSManagedObject>: Publisher {
typealias Output = CollectionDifference<Object>
typealias Failure = Error
let fetchRequest: NSFetchRequest<Object>
let context: NSManagedObjectContext
init(fetchRequest: NSFetchRequest<Object>, context: NSManagedObjectContext) {
self.fetchRequest = fetchRequest
self.context = context
}
func receive<S: Subscriber>(subscriber: S) where Failure == S.Failure, Output == S.Input {
let inner = Inner(downstream: subscriber, fetchRequest: fetchRequest, context: context)
subscriber.receive(subscription: inner)
}
private final class Inner<Downstream: Subscriber>: NSObject, Subscription,
NSFetchedResultsControllerDelegate
where Downstream.Input == CollectionDifference<Object>, Downstream.Failure == Error {
private let downstream: Downstream
private var fetchedResultsController: NSFetchedResultsController<Object>?
init(
downstream: Downstream,
fetchRequest: NSFetchRequest<Object>,
context: NSManagedObjectContext
) {
self.downstream = downstream
fetchedResultsController
= NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: context,
sectionNameKeyPath: nil,
cacheName: nil)
super.init()
fetchedResultsController!.delegate = self
do {
try fetchedResultsController!.performFetch()
updateDiff()
} catch {
downstream.receive(completion: .failure(error))
}
}
private var demand: Subscribers.Demand = .none
func request(_ demand: Subscribers.Demand) {
self.demand += demand
fulfillDemand()
}
private var lastSentState: [Object] = []
private var currentDifferences = CollectionDifference<Object>([])!
private func updateDiff() {
currentDifferences
= Array(fetchedResultsController?.fetchedObjects ?? []).difference(
from: lastSentState)
fulfillDemand()
}
private func fulfillDemand() {
if demand > 0 && !currentDifferences.isEmpty {
let newDemand = downstream.receive(currentDifferences)
lastSentState = Array(fetchedResultsController?.fetchedObjects ?? [])
currentDifferences = lastSentState.difference(from: lastSentState)
demand += newDemand
demand -= 1
}
}
func cancel() {
fetchedResultsController?.delegate = nil
fetchedResultsController = nil
}
func controllerDidChangeContent(
_ controller: NSFetchedResultsController<NSFetchRequestResult>
) {
updateDiff()
}
override var description: String {
"ManagedObjectChanges(\(Object.self))"
}
}
}
@michaello
Copy link

Are you sure that line 81 is correct? We're applying difference on the same arrays: lastSentState.

@mjm
Copy link
Author

mjm commented Oct 1, 2020

@michaello Yeah, it looks super weird, but that is actually correct!

That line is just resetting currentDifferences to an empty state. I couldn't tell you why I opted to express that in this particular way, I guess I felt like the initializer for it was clunky.

@dpereira411
Copy link

@mjm How do this handle field changes in a NSManagedObject?

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