Created
November 3, 2019 20:41
-
-
Save mjm/750b20e1dfd5b1abc82b8295b54b3c74 to your computer and use it in GitHub Desktop.
Observe changes to a Core Data fetch request with Combine
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 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.
@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
Are you sure that line 81 is correct? We're applying difference on the same arrays:
lastSentState
.