Reputation: 17081
I am using the "old-school" Core Data concurrency model. In other words, I have a main NSManagedObjectContext
that is created with NSMainQueueConcurrencyType
and any network requests process JSON response data in a freshly created NSPrivateQueueConcurrencyType
context. The contexts share a persistentStoreCoordinator
instead of using a parentContext
and context merging is done manually.
Since iOS 8, some of my devices have exhibited a behavior that seems context merging is not working correctly. After observing a NSManagedObjectContextDidSaveNotification
, I am merging changes into my main context by calling mergeChangesFromContextDidSaveNotification
on my main context. Afterwards, I attempt to grab the main context versions of the active object graph with objectWithID:
which superficially appears to work. However, upon close inspection of the objects returned, any NSSet
relationships are empty even though the secondary context version has them.*
Oddly, the same code produces accurate context merging on an iPhone 6/6+ running iOS 8. Even my iOS 7 iPod Touch 5G works correctly. The device that consistently fails is an identical iOS 8 iPod Touch 5G. It works in all simulators as well.
Has anyone else seen a similar behavior or have insight as to what might cause this device-specific Core Data issue? Thanks in advance.
__block id toReturn = [self processResponse:responseData]; //Complete object graph from api.
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[context setPersistentStoreCoordinator:[self persistentStoreCoordinator]]; //shared PSC
[context setUndoManager:nil];
[context setStalenessInterval:0];
[context setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
NSManagedObjectContext *mainContext = [self mainContext];
[[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification
object:context
queue:[NSOperationQueue mainQueue]
usingBlock:
^(NSNotification *notification) {
[mainContext mergeChangesFromContextDidSaveNotification:notification];
NSManagedObjectID *managedId = [toReturn objectID];
NSManagedObject *mainContextVersion = [mainContext objectWithID:managedId];
toReturn = mainContextVersion; //*This is where the object graph is incomplete
}];
//Save context, will fire NSManagedObjectContextDidSaveNotification.
NSError *errorWhileSaving;
[context save:&errorWhileSaving];
Upvotes: 0
Views: 362
Reputation: 28409
I would not play the game you appear to be playing with toReturn
where it looks like it starts out as a managed object in the private context, and becomes a managed object in the main context.
You should use a managed object ID as the global block variable (or, better yet, no global at all) so you don't have to peek into the managed object to get it. I've seen a number of claims that you can get the ObjectID without using performBlock
but I never touch any MO/MOC outside its performBlock
. Never. Ever. Too many headaches in the past, so I just stick to my guns.
__block NSManagedObjectID *toReturnObjectID;
However, if you want to do that, you must access it from the proper context first. At first blush, something like this... which is very ugly, but demonstrates the point...
[mainContext mergeChangesFromContextDidSaveNotification:notification];
[toReturn.context performBlock: ^{
NSManagedObjectID *managedId = [toReturn objectID];
[mainContext performBlock: ^{
NSManagedObject *mainContextVersion = [mainContext objectWithID:managedId];
toReturn = mainContextVersion; //*This is where the object graph is incomplete
// All the rest of the code that needs to happen...
Note, also, that any access of your global toReturn
after calling save in the other context could spell trouble. Basically, you should not do that. You already fire a notification when processing is complete. Why not put the toReturn
in the notification user info, rather than piling it into a global variable? Also, you could put the ObjectID in the userInfo of the context posting the did-save notification, and grab it out of there when the notification is fired.
If you let the notification run in its own thread (not give a queue for the block to run on), you can access the saving MOC because you are running in its active thread/queue/context. Shove the object ID into the userInfo before saving.
Something like...
[[NSNotificationCenter defaultCenter]
addObserverForName:NSManagedObjectContextDidSaveNotification
object:context
queue:nil
usingBlock:^(NSNotification *notification) {
// we can access the context here, since we are running in its notification
NSManagedObjectContext *context = [notification object];
NSManagedObjectID *managedId = [[context userInfo] objectForKey:@"ObjectGraphOID"];
// Now that we have the object ID, capture it in the block that will do the work...
[mainContext performBlock:^{
[mainContext mergeChangesFromContextDidSaveNotification:notification];
NSManagedObject *mainContextVersion = [mainContext objectWithID:managedId];
NSMutableDictionary *mainContextUserInfo =
[[self migratedInsertedObjectsDictionaryFromUserInfo:[notification userInfo]] mutableCopy];
// Pass the result as part of userInfo of the notification
mainContextUserInfo[@"ObjectGraph"] = mainContextVersion;
[[NSNotificationCenter defaultCenter]
postNotificationName:FPFlatpackCycleCompleted
object:self
userInfo:mainContextUserInfo];
}];
}];
Upvotes: 1