I'm facing a NSFetchResultController behaviour difference given two different coredata managed object context stack.
First scenario: coredata stack is nested, i.e. mainContext is parent of all background contexts created for each of our tasks managing our data. The main context's parent is a separate background context just for saving on background thread the main context and avoid blocking the ui.
The main context is created as following in this nested case:
let psc = NSPersistentStoreCoordinator(managedObjectModel: mom)
// We create 2 contexts: one private to save in persistentStore, on a background thread, and one main. See [this design](https://cocoacasts.com/core-data-and-concurrency/) for more details.
persistentStoreContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
persistentStoreContext.persistentStoreCoordinator = psc
persistentStoreContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
mainContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
mainContext.parent = persistentStoreContext
The background context is created as following in this nested case:
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.parent = mainContext
I have background task that owns background Coredata context in which I modify some entity then I save the context so that change is propagated to main given the parent relationship. In this case I get a didChange called on my NSFetchResultController delegate with "update" change key and a non empty changedValues() from the object that has been actually changed from my background context.
public func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .delete:
break
case .insert:
break
case .move:
break
case .update:
if let channel = anObject as? Channel,
let channelVM = channelViewModels[channel.epgId]?.viewModel {
if channel.changedValues()["lastImportStartDate"] != nil {
channelVM.resetFRC()
channelVM.viewModelShouldUpdateData(self)
}
So far so good, I can detect what has been changed and decide if I need to reload my ui.
Failing second scenario: stack context is sibling, both main and background context have coordinator for parent hence no relation between contexts.
The main context is created as following in this sibling case:
let psc = NSPersistentStoreCoordinator(managedObjectModel: mom)
mainContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
The private context for our background tasks is created as following in this sibling case:
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
On my background task I modify a modeled property (not transient) then I save the context that propagates change to store with the following code :
open func saveWithSiblingStack(completion: #escaping ((Result<Bool, Error>) -> Void)) {
guard let context = context else {
completion(Result.success(true)) // leads to didFinish
return
}
// If sibling stack is used, mainContext is not
// set to automaticallyMergeFrom parent as we handle this
// below to make sure didFinish is called after maincontext having
// merged changes from this task background context
context.perform {
if context.hasChanges {
let observer = NotificationCenter.default.addObserver(forName: .NSManagedObjectContextDidSave, object: context, queue: .main) { notification in
self.dataController.mainContext.performAndWait {
self.dataController.mainContext.mergeChanges(fromContextDidSave: notification)
}
self.queue.async {
completion(Result.success(true))
}
}
do {
try context.save()
} catch {
completion(Result.failure(error))
}
NotificationCenter.default.removeObserver(observer)
} else {
completion(Result.success(true))
}
}
}
In this configuration, my NSFetchResultController delegate is called with a didChange "update" but the changedValues() array is empty.
Does someone knows why in that case I don't get the detail of what has been changed from the background context ? This is especially weird knowing that if I subscribe to the merge notification manually (i.e. NotificationCenter.default.addObserver(forName: .NSManagedObjectContextDidMergeChangesObjectIDs, object: nil, queue: .main)) in the same class holding my fetch result controller I actually get the detail of what has been change from the merge via the notification.userInfo?[NSUpdatedObjectsKey] dic and changedValues() is not empty !
let observer = NotificationCenter.default.addObserver(forName: .NSManagedObjectContextDidMergeChangesObjectIDs, object: nil, queue: .main) { notification in
if let objects = notification.userInfo?[NSUpdatedObjectsKey] as? Set<NSManagedObject> {
for obj in objects {
guard let channel = obj as? Channel else { continue }
if obj.changedValues()["lastImportStartDate"] != nil {
// here contrary to NSFetchResultController, obj.changedValues() is not empty with the sibling stack
Of course I searched stackoverflow for similar case without success and apple developer forum.
I tested different NSFetchResultController options (with, without cache) and fetch request options as well (includePendingChange ...) but I always get changesValues() empty.
Many thanks in advance for any help or link to external resources I may have not already seen.
Best regards
Cédric
Related
Currently, there are 2 NSManagedObjectContext used in our app
CoreDataStack.INSTANCE.persistentContainer.viewContext
CoreDataStack.INSTANCE.backgroundContext
class CoreDataStack {
static let INSTANCE = CoreDataStack()
private init() {
}
private(set) lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "wenote")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// This is a serious fatal error. We will just simply terminate the app, rather than using error_log.
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
// So that when backgroundContext write to persistent store, container.viewContext will retrieve update from
// persistent store.
container.viewContext.automaticallyMergesChangesFromParent = true
return container
}()
private(set) lazy var backgroundContext: NSManagedObjectContext = {
let backgroundContext = persistentContainer.newBackgroundContext()
// Similar behavior as Android's Room OnConflictStrategy.REPLACE
// Old data will be overwritten by new data if index conflicts happen.
backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return backgroundContext
}()
}
We build a NSFetchedResultsController around CoreDataStack.INSTANCE.persistentContainer.viewContext
Even when the write operation is performed using CoreDataStack.INSTANCE.backgroundContext, NSFetchedResultsController is still able to receive notification, due to the following line
// So that when backgroundContext write to persistent store, container.viewContext will retrieve update from
// persistent store.
container.viewContext.automaticallyMergesChangesFromParent = true
When I try to observe Core Data change manually, via
notificationCenter.addObserver(
self,
selector: #selector(managedObjectContextDidSave),
name: NSNotification.Name.NSManagedObjectContextDidSave,
object: viewContext
)
We do not receive notification when backgroundContext perform write.
It will only work if we observe using the following way
notificationCenter.addObserver(
self,
selector: #selector(managedObjectContextDidSave),
name: NSNotification.Name.NSManagedObjectContextDidSave,
object: backgroundContext
)
May I know why it is so? Why automaticallyMergesChangesFromParent work for NSFetchedResultsController, but not the manual NotificationCenter observing?
Let me suppose that this happens because viewContext does not actually save anything. It just reads back from parent - persistent store coordinator. It is the backgroundContext that saves to itself (and to its parent - persistent store coordinator).
NSNotification.Name.NSManagedObjectContextDidMergeChangesObjectIDs
Might work to observe merging into viewContext.
I would like to react to modified NSManagedObjects, therefore I setup an observer:
NotificationCenter.default.addObserver(forName: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: nil, queue: nil) { notification in
...
}
But I didn't find a solution yet how to create objects inside that block.
NotificationCenter.default.addObserver(forName: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: nil, queue: nil) { notification in
let context = notification.object as! NSManagedObjectContext
context.perform {
let insertedObjects = notification.userInfo?[NSInsertedObjectsKey] as? Set<NSManagedObject> ?? Set<NSManagedObject>()
// insertedObjects are empty (outside of context.perform they are NOT EMPTY
}
}
Also when I do not use context.perform I do get attempt to recursively call -save: on the context aborted. How can I achieve this?
I was trying to fetch realm data on the background thread and add a notification block (iOS, Swift).
Basic example:
func initNotificationToken() {
DispatchQueue.global(qos: .background).async {
let realm = try! Realm()
results = self.getRealmResults()
notificationToken = results.addNotificationBlock { [weak self] (changes: RealmCollectionChange) in
switch changes {
case .initial:
self?.initializeDataSource()
break
case .update(_, let deletions, let insertions, let modifications):
self?.updateDataSource(deletions: deletions, insertions: insertions, modifications: modifications)
break
case .error(let error):
fatalError("\(error)")
break
}
}
}
}
func initializeDataSource() {
// process the result set data
DispatchQueue.main.async(execute: { () -> Void in
// update UI
})
}
func updateDataSource(deletions: [Int], insertions: [Int], modifications: [Int]) {
// process the changes in the result set data
DispatchQueue.main.async(execute: { () -> Void in
// update UI
})
}
When doing this I get
'Can only add notification blocks from within runloops'
I have to do some more extensive processing with the returned data and would like to only go back to the main thread when updating the UI after the processing is done.
Another way would probably to re-fetch the data after any update on the background thread and then do the processing, but it feels like avoidable overhead.
Any suggestions on the best practice to solve this?
To add a notification on a background thread you have to manually run a run loop on that thread and add the notification from within a callout from that run loop:
class Stuff {
var token: NotificationToken? = nil
var notificationRunLoop: CFRunLoop? = nil
func initNotificationToken() {
DispatchQueue.global(qos: .background).async {
// Capture a reference to the runloop so that we can stop running it later
notificationRunLoop = CFRunLoopGetCurrent()
CFRunLoopPerformBlock(notificationRunLoop, CFRunLoopMode.defaultMode.rawValue) {
let realm = try! Realm()
results = self.getRealmResults()
// Add the notification from within a block executed by the
// runloop so that Realm can verify that there is actually a
// runloop running on the current thread
token = results.addNotificationBlock { [weak self] (changes: RealmCollectionChange) in
// ...
}
}
// Run the runloop on this thread until we tell it to stop
CFRunLoopRun()
}
}
deinit {
token?.stop()
if let runloop = notificationRunLoop {
CFRunLoopStop(runloop)
}
}
}
GCD does not use a run loop on its worker threads, so anything based on dispatching blocks to the current thread's run loop (such as Realm's notifications) will never get called. To avoid having notifications silently fail to do anything Realm tries to check for this, which unfortunately requires the awakward PerformBlock dance.
I have a private NSManagedObjectContext queue that I'm using to save an entity to Core Data. After it has finished saving, I want to send out an NSNotification. However, it doesn't seem to like me sending out the notification from the private queue. This is my code for the private queue:
let parentManagedContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext!
let privateManagedContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
privateManagedContext.persistentStoreCoordinator = parentManagedContext.persistentStoreCoordinator
privateManagedContext.performBlock {
...
// Save the entity
do {
try privateManagedContext.save()
// Send out NSNotification here
}
}
How do I add a block within performBlock to run on the main thread?
Ok so minutes after I posted this question I figured out the answer. All I had to do was add this code after the try privateManagedContext.save() code:
NSOperationQueue.mainQueue().addOperationWithBlock({
NSNotificationCenter.defaultCenter().postNotificationName(kNotificationName, object: nil)
})
Hope this helps
dispatch_async(dispatch_get_main_queue()) {
NSNotificationCenter.defaultCenter().postNotificationName(kNotificationName, object: nil)
}
I am writing one program on iOS and very race I am facing this error:
2015-11-06 10:57:24.289 NETFNET[2503:976392] CoreData: error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. -[__NSCFSet addObject:]: attempt to insert nil with userInfo (null)
2015-11-06 10:57:24.293 NETFNET[2503:976392] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFSet addObject:]: attempt to insert nil'
I am trying to access Data Base simultaneously, I think, from main and background threads. I have seen a lot of solutions for Objective C, but none for Swift (I don't know Objective C...). Unfortunately, I don't know how to work with Grand Central Dispatch and, in fact, my program does not really need several treads (I mean it need it, but if some thread loses info from one function for one time, nothing bad will happen). I just want to have stable program on Swift 1 or 2, so I will be thankful for any help.
You need to create a private NSManagedObjectContext with private queue concurrency type and use it to access CoreData whenever operating on a background thread.
So suppose I need to run a database operation on the background, I can dispatch that work to the background thread as
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), {
//call your background operation.
})
Then in the background operation I can create a private NSManagedObjectContext as
let moc = … //Our primary context on the main queue
let privateMOC = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
privateMOC.parentContext = moc
privateMOC.performBlock {
//operations
do {
try privateMOC.save()
} catch {
fatalError("Failure to save context: \(error)")
}
}
Read through Apple's CoreData Concurrency Guide to get a good understanding before implementing core data operations on multiple threads.
Very good. I tried it, it worked fine for me.
Thank you very much.
Previous Code:
do {
try CDHelper.shared.context.save()
}
catch let error as NSError {
// Error mesajlarını ekle!!
print("Could not fetch \(error), \(error.localizedDescription)")
print("Could not fetch \(error), \(error.localizedFailureReason)")
}
// MARK: - CONTEXT
lazy var context: NSManagedObjectContext = {
let moc = NSManagedObjectContext(concurrencyType:.MainQueueConcurrencyType)
moc.persistentStoreCoordinator = self.coordinator
return moc
}()
// MARK: - MODEL
lazy var model: NSManagedObjectModel = {
return NSManagedObjectModel(contentsOfURL:self.modelURL)!
}()
// MARK: - COORDINATOR
lazy var coordinator: NSPersistentStoreCoordinator = {
return NSPersistentStoreCoordinator(managedObjectModel:self.model)
}()
lazy var modelURL: NSURL = {
let bundle = NSBundle.mainBundle()
if let url = bundle.URLForResource("Model", withExtension: "momd") {
return url
}
print("CRITICAL - Managed Object Model file not found")
abort()
}()
you should change the code this way:
let moc = NSManagedObjectContext(concurrencyType:.MainQueueConcurrencyType)
let privateMOC = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
privateMOC.parentContext = moc
privateMOC.performBlock({
do {
try privateMOC.save()
} catch {
fatalError("Failure to save context: \(error)")
}
})
Just calling your CoreData Function inside
DispatchQueue.main.async {
...
}
worked for me
You can create an extension for this and wrap the save() function to something like this so you'll just need to use this function instead of save():
extension NSManagedObjectContext {
func update() throws {
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.parent = self
context.perform({
do {
try context.save()
} catch {
print(error)
}
})
}
}