What I'm trying to do in a nutshell, I use a background queue to save JSON objects pulled from a web service into a Core Data Sqlite3 database. Saving occurs in a serialized background queue created using GCD and is saved in the secondary instance of NSManagedObjectContext that is created for this background queue. After the save is complete, I need to update the NSManagedObjectContext instance, which is in the main thread with the newly created / updated objects. The problem I'm experiencing is an instance of NSManagedObjectContext in the main thread that cannot find objects that were stored in the background context. Below is a list of the actions that I take with the code samples. Any thoughts on what I'm doing wrong?
- Create a background queue via GCD, run all the preprocessing logic, and then save the background context in this thread:
.
// process in the background queue dispatch_async(backgroundQueue, ^(void){ if (savedObjectIDs.count > 0) { [savedObjectIDs removeAllObjects]; } if (savedObjectClass) { savedObjectClass = nil; } // set the thead name NSThread *currentThread = [NSThread currentThread]; [currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME]; // if there is not already a background context, then create one if (!_backgroundQueueManagedObjectContext) { NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; if (coordinator != nil) { _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init]; [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator]; } } // save the JSON dictionary starting at the upper most level of the key path, and return all created/updated objects in an array NSArray *objectIds = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0]; // save the object IDs and the completion block to global variables so we can access them after the save if (objectIds) { [savedObjectIDs addObjectsFromArray:objectIds]; } if (completion) { saveCompletionBlock = completion; } if (managedObjectClass) { savedObjectClass = managedObjectClass; } // save all changes object context [self saveManagedObjectContext]; });
The saveManagedObjectContext method basically examines which thread is running and saves the appropriate context. I checked that this method works correctly, so I will not post the code here.
All this code is in singleton mode, and in the singleton init method I add a listener for "NSManagedObjectContextDidSaveNotification" and call the mergeChangesFromContextDidSaveNotification method:
.
// merge changes from the context did save notification to the main context - (void)mergeChangesFromContextDidSaveNotification:(NSNotification *)notification { NSThread *currentThread = [NSThread currentThread]; if ([currentThread.name isEqual:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME]) { // merge changes to the primary context, and wait for the action to complete on the main thread [_managedObjectContext performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES]; // on the main thread fetch all new data and call the completion block dispatch_async(dispatch_get_main_queue(), ^{ // get objects from the database NSMutableArray *objects = [[NSMutableArray alloc] init]; for (id objectID in savedObjectIDs) { NSError *error; id object = [_managedObjectContext existingObjectWithID:objectID error:&error]; if (error) { [self logError:error]; } else if (object) { [objects addObject:object]; } } // remove all saved object IDs from the array [savedObjectIDs removeAllObjects]; savedObjectClass = nil; // call the completion block //completion(objects); saveCompletionBlock(objects); // clear the saved completion block saveCompletionBlock = nil; }); } }
As you can see in the above method, I call "mergeChangesFromContextDidSaveNotification:" in the main thread, and I decided that the action would wait until completion. According to Apple's documentation, the background thread must wait until this action is complete before it can continue with the rest of the code below this call. As I mentioned above, I run this code, everything works, but when I try to print the selected objects to the console, I get nothing. It seems like the merge does not actually happen or may not end before my code ends. Is there another notice I should listen to make sure the merge is complete? Or do I need to keep the main context of the object after merging, but before fecth?
In addition, I apologize for the incorrect formatting of the code, but it seems that SO code tags do not like method definitions.
Thanks guys!
UPDATE:
I made the changes that were recommended below, but still have the same problem. Below is the updated code.
This is the code that causes processes to save background flow.
// process in the background queue dispatch_async(backgroundQueue, ^(void){ if (savedObjectIDs.count > 0) { [savedObjectIDs removeAllObjects]; } if (savedObjectClass) { savedObjectClass = nil; } // set the thead name NSThread *currentThread = [NSThread currentThread]; [currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME]; // if there is not already a background context, then create one if (!_backgroundQueueManagedObjectContext) { NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; if (coordinator != nil) { _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init]; [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator]; } } // save the JSON dictionary starting at the upper most level of the key path NSArray *objectIds = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0]; // save the object IDs and the completion block to global variables so we can access them after the save if (objectIds) { [savedObjectIDs addObjectsFromArray:objectIds]; } if (completion) { saveCompletionBlock = completion; } if (managedObjectClass) { savedObjectClass = managedObjectClass; } // listen for the merge changes from context did save notification [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChangesFromBackground:) name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext]; // save all changes object context [self saveManagedObjectContext]; });
This is the code called by the NSManagedObjectContextDidSaveNotification notification
// merge changes from the context did save notification to the main context - (void)mergeChangesFromBackground:(NSNotification *)notification { // kill the listener [[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext]; NSThread *currentThread = [NSThread currentThread]; // merge changes to the primary context, and wait for the action to complete on the main thread [[self managedObjectContext] performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES]; // dispatch the completion block dispatch_async(dispatch_get_main_queue(), ^{ // get objects from the database NSMutableArray *objects = [[NSMutableArray alloc] init]; for (id objectID in savedObjectIDs) { NSError *error; id object = [[self managedObjectContext] existingObjectWithID:objectID error:&error]; if (error) { [self logError:error]; } else if (object) { [objects addObject:object]; } } // remove all saved object IDs from the array [savedObjectIDs removeAllObjects]; savedObjectClass = nil; // call the completion block //completion(objects); saveCompletionBlock(objects); // clear the saved completion block saveCompletionBlock = nil; }); }
UPDATE:
So, I have found a solution. It turns out that the way I saved the identifiers of objects in the background thread, and then tried to use them in the main thread to retrieve them again, did not work. So I ended up pulling the inserted / updated objects from the userInfo dictionary, which is sent with an NSManagedObjectContextDidSaveNotification notification. Below is my updated code, which now works.
As before, this code triggers the pre-warning and save logic
// process in the background queue dispatch_async(backgroundQueue, ^(void){ // set the thead name NSThread *currentThread = [NSThread currentThread]; [currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME]; [self logMessage:[NSString stringWithFormat:@"(%@) saveJSONObjects:objectMapping:class:completion:", [managedObjectClass description]]]; // if there is not already a background context, then create one if (!_backgroundQueueManagedObjectContext) { NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; if (coordinator != nil) { _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init]; [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator]; } } // save the JSON dictionary starting at the upper most level of the key path [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0]; // save the object IDs and the completion block to global variables so we can access them after the save if (completion) { saveCompletionBlock = completion; } // listen for the merge changes from context did save notification [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChangesFromBackground:) name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext]; // save all changes object context [self saveManagedObjectContext]; });
This is a modified method that processes NSManagedObjectContextDidSaveNotification.
- (void)mergeChangesFromBackground:(NSNotification *)notification { // kill the listener [[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext]; // merge changes to the primary context, and wait for the action to complete on the main thread [[self managedObjectContext] performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES]; // dispatch the completion block dispatch_async(dispatch_get_main_queue(), ^{ // pull the objects that were saved from the notification so we can get them on the main thread MOC NSDictionary *userInfo = [notification userInfo]; NSMutableArray *modifiedObjects = [[NSMutableArray alloc] init]; NSSet *insertedObject = (NSSet *)[userInfo objectForKey:@"inserted"]; NSSet *updatedObject = (NSSet *)[userInfo objectForKey:@"updated"]; if (insertedObject && insertedObject.count > 0) { [modifiedObjects addObjectsFromArray:[insertedObject allObjects]]; } if (updatedObject && updatedObject.count > 0) { [modifiedObjects addObjectsFromArray:[updatedObject allObjects]]; } NSMutableArray *objects = [[NSMutableArray alloc] init]; // iterate through the updated objects and find them in the main thread MOC for (NSManagedObject *object in modifiedObjects) { NSError *error; NSManagedObject *obj = [[self managedObjectContext] existingObjectWithID:object.objectID error:&error]; if (error) { [self logError:error]; } if (obj) { [objects addObject:obj]; } } modifiedObjects = nil; // call the completion block saveCompletionBlock(objects); // clear the saved completion block saveCompletionBlock = nil; }); }