Nested CoreData contexts in multithreaded iOS application
You have a big app with CoreData store, NSFetchedResultsController's, huge networking layer & some background updates. Everything should work smoothly: data updates should be independent of each other, your table views should always show latest data available, etc.
How to organize your CoreData stack properly to avoid any kind of lagging and filesystem access errors?
- (NSManagedObjectContext*)masterObjectContext { if (_masterObjectContext != nil) { return _masterObjectContext; } _masterObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; [_masterObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinator]; return _masterObjectContext; }
Note the private concurrency type here. This will allow it to do all the disk I/O operations in background, without blocking the UI for a millisecond.
Then create a main context for the UI thread, and attach it to the master context as a child:
- (NSManagedObjectContext*)mainObjectContext{ if (_mainObjectContext != nil) { return _mainObjectContext; } _mainObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; [_mainObjectContext setParentContext:[self masterObjectContext]]; return _mainObjectContext; }
This one is the entry point for all your table views with fetch controllers. This one will always keep the UI up to date without any additional work from your side. To hell all those notifications & merges. Everything that is saved to the main context becomes automatically visible.
Then create your worker contexts with private queues and attach them to the main context:
- (NSManagedObjectContext*)privateObjectContext { NSManagedObjectContext* objectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; [objectContext setParentContext:[self mainObjectContext]]; return objectContext; }
Those are the things that you do your background updates with. You download all the data (in background, of course), then you push all the data to your private context and call it a day:
[privateContext performBlock: ^{ NSError* error = nil; [privateContext save:&error]; if (error) { DDLogError(@"Error while propagating changes to parent context: %@", error.localizedDescription); } }];
Now the data is kind of saved, but not yet propagated to the main context. How to get it there? Just save the main context:
[privateContext performBlock: ^{ NSError* error = nil; [privateContext save:&error]; if (error) { DDLogError(@"Error while propagating changes to parent context: %@", error.localizedDescription); } else { NSManagedObjectContext* mainContext = privateContext.parentContext; [mainContext performBlock: ^{ // Push changes to a parent context, it it exists. NSError* error = nil; [mainContext save:&error]; if (error) { DDLogError(@"Error while propagating changes to parent context: %@", error.localizedDescription); } }]; } }];
Simple, right? So finally, how to get data to the store? You guessed right - just save the master context. Nice and easy.
If you don't want to do this manually, you can set up a single listener in your CoreData stack manager and do this on every notification:
_observerId = [[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:nil queue:nil usingBlock:^(NSNotification* note) { if ([note.object parentContext] != nil) { [[note.object parentContext] performBlock: ^{ // Push changes to a parent context, it it exists. NSError* error = nil; [[note.object parentContext] save:&error]; if (error) { DDLogError(@"Error while propagating changes to parent context: %@", error.localizedDescription); } }]; } }];