Question: How do I get my child context to see changes persisted on the parent context so that they trigger my NSFetchedResultsController to update the UI?
Here's the setup:
You've got an app that downloads and adds lots of XML data (about 2 million records, each roughly the size of a normal paragraph of text) The .sqlite file becomes about 500 MB in size. Adding this content into Core Data takes time, but you want the user to be able to use the app while the data loads into the data store incrementally. It's got to be invisible and imperceptible to the user that large amounts of data are being moved around, so no hangs, no jitters: scrolls like butter. Still, the app is more useful, the more data is added to it, so we can't wait forever for the data to be added to the Core Data store. In code this means I'd really like to avoid code like this in the import code:
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];
The app is iOS 5 only so the slowest device it needs to support is an iPhone 3GS.
Here are the resources I've used so far to develop my current solution:
Apple's Core Data Programming Guide: Efficiently Importing Data
- Use Autorelease Pools to keep the memory down
- Relationships Cost. Import flat, then patch up relationships at the end
- Don't query if you can help it, it slows things down in an O(n^2) manner
- Import in Batches: save, reset, drain and repeat
- Turn off the Undo Manager on import
iDeveloper TV - Core Data Performance
- Use 3 Contexts: Master, Main and Confinement context types
iDeveloper TV - Core Data for Mac, iPhone & iPad Update
- Running saves on other queues with performBlock makes things fast.
- Encryption slows things down, turn it off if you can.
Importing and Displaying Large Data Sets in Core Data by Marcus Zarra
- You can slow down the import by giving time to the current run loop, so things feel smooth to the user.
- Sample Code proves that it is possible to do large imports and keep the UI responsive, but not as fast as with 3 contexts and async saving to disk.
My Current Solution
I've got 3 instances of NSManagedObjectContext:
masterManagedObjectContext - This is the context that has the NSPersistentStoreCoordinator and is responsible for saving to disk. I do this so my saves can be asynchronous and therefore very fast. I create it on launch like this:
masterManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[masterManagedObjectContext setPersistentStoreCoordinator:coordinator];
mainManagedObjectContext - This is the context the UI uses everywhere. It is a child of the masterManagedObjectContext. I create it like this:
mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[mainManagedObjectContext setUndoManager:nil];
[mainManagedObjectContext setParentContext:masterManagedObjectContext];
backgroundContext - This context is created in my NSOperation subclass that is responsible for importing the XML data into Core Data. I create it in the operation's main method and link it to the master context there.
backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[backgroundContext setUndoManager:nil];
[backgroundContext setParentContext:masterManagedObjectContext];
This actually works very, VERY fast. Just by doing this 3 context setup I was able to improve my import speed by over 10x! Honestly, this is hard to believe. (This basic design should be part of the standard Core Data template...)
During the import process I save 2 different ways. Every 1000 items I save on the background context:
BOOL saveSuccess = [backgroundContext save:&error];
Then at the end of the import process, I save on the master/parent context which, ostensibly, pushes modifications out to the other child contexts including the main context:
[masterManagedObjectContext performBlock:^{
NSError *parentContextError = nil;
BOOL parentContextSaveSuccess = [masterManagedObjectContext save:&parentContextError];
}];
Problem: The problem is that my UI will not update until I reload the view.
I have a simple UIViewController with a UITableView that is being fed data using a NSFetchedResultsController. When the Import process completes, the NSFetchedResultsController see's no changes from the parent/master context and so the UI doesn't automatically update like I'm used to seeing. If I pop the UIViewController off the stack and load it again all the data is there.
Question: How do I get my child context to see changes persisted on the parent context so that they trigger my NSFetchedResultsController to update the UI?
I have tried the following which just hangs the app:
- (void)saveMasterContext {
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self selector:@selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
NSError *error = nil;
BOOL saveSuccess = [masterManagedObjectContext save:&error];
[notificationCenter removeObserver:self name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
}
- (void)contextChanged:(NSNotification*)notification
{
if ([notification object] == mainManagedObjectContext) return;
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:@selector(contextChanged:) withObject:notification waitUntilDone:YES];
return;
}
[mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}