ASJCoreDataOperation
Adding concurrency/multi-threading to CoreData is not very straightforward and obvious. The main issue is with NSManagedObjectContext, which is thread unsafe. The default one created in AppDelegate is created on the main thread:
Swift:
var managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)Objective-C:
NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];The main drawback of using AppDelegate's managed object context is that whenever a save, fetch or delete operation is performed, the main thread/UI will get blocked. If you are doing small operations, this may not be noticeable. But for larger operations, it will pose a problem.
The solution is to do such CoreData operations in the background and only when you have to do any UI changes, say reloading a table, you call reloadData on the main queue.
Installation
CocoaPods is the preferred way to install this library. Add this command to your Podfile:
Swift:
pod 'ASJCoreDataOperation'Objective-C:
pod 'ASJCoreDataOperation/Obj-C'Background
- Key:
NSManagedObjectContext=moc
Concurrency options
There are three concurrency types:
NSConfinementConcurrencyType(which is marked deprecated from iOS 9.0)NSPrivateQueueConcurrencyTypeNSMainQueueConcurrencyType
You should not use NSConfinementConcurrencyType anymore since it's obsolete and Apple doesn't recommend it. NSPrivateQueueConcurrencyType creates an moc on a background thread and NSMainQueueConcurrencyType creates one on the main queue. The one we are interested in is NSPrivateQueueConcurrencyType.
Creating an NSManagedObjectContext
You can create as many mocs as you wish. During saving, it must go through an NSPersistentStoreCoordinator to write data to an sqlite file. You can use the one implemented in AppDelegate or provide your own. Just make sure that no matter what kind of store your app has; NSSQLiteStoreType, NSXMLStoreType, NSBinaryStoreType or NSInMemoryStoreType, the coordinator object must be tied to the same destination.
Swift:
var privateMoc = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateMoc.persistentStoreCoordinator = appDelegatesPersistentStoreCoordinator;Objective-C:
NSManagedObjectContext *privateMoc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
privateMoc.persistentStoreCoordinator = appDelegatesPersistentStoreCoordinator;There are two methods for mocs, performBlock:(void (^)())block and performBlockAndWait:(void (^)())block. Any code written in those blocks is guaranteed to be executed on the same queue the moc is created. You must write your CoreData logic inside one of these methods. The difference between the two is that performBlockAndWait: will block the queue until its operation is completed.
Saving on a private queue
Whenever a save happens on a private moc, data will be written to the sqlite file but the main queue will not be notified about it. If you have an NSFetchedResultsController setup on the main queue, control will not reach its delegate methods. However, if the CoreData operation and NSFetchedResultsController share the same moc, it will work.
If you need the main queue to be notified about any changes made by a private context, you need to merge those changes from the private moc to the main moc. To do so, you have to start observing for NSManagedObjectContextDidSaveNotification on the private moc.
Swift:
NotificationCenter.default.addObserver(self, selector: #selector(contextDidSave(note:)), name: .NSManagedObjectContextDidSave, object: privateMoc)Objective-C:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contextDidSave:) name:NSManagedObjectContextDidSaveNotification object:privateMoc];In contextDidSave: we need to merge the private moc changes into the main moc. You MUST use the moc's performBlock: or performBlockAndWait: methods to ensure the merge happens on the correct thread.
Swift:
func contextDidSave(note: Notification)
{
mainMoc.perform { () -> Void in
self.mainMoc.mergeChanges(fromContextDidSave: note)
}
}Objective-C:
- (void)contextDidSave:(NSNotification *)note
{
[mainMoc performBlock:^{
[mainMoc mergeChangesFromContextDidSaveNotification:note];
}];
}The note object has information of all modifications made to the managed objects. There can be issues however when merging data between two mocs and conflicts may arise. So, you must provide a mergePolicy so that CoreData knows how to resolve them.
Swift:
privateMoc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicyObjective-C:
privateMoc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;- Note: There is another concurrency pattern, using child and parent
mocs which has a simpler setup but it is not recommended because it blocks the main queue.
What this library does
ASJCoreDataOperation is a subclass of Operation/NSOperation that provides private queue support out of the box. This class is designed to be subclassed and will not work without it.
Swift:
convenience init(privateMoc: NSManagedObjectContext!, mainMoc: NSManagedObjectContext?)Objective-C:
- (instancetype)initWithPrivateMoc:(nullable NSManagedObjectContext *)privateMoc mainMoc:(nullable NSManagedObjectContext *)mainMoc NS_DESIGNATED_INITIALIZER;This is the recommended way to create an instance of your subclass. You may pass nil in both arguments or use the init method. In those cases, a private moc will be created and AppDelegate's moc will be accessed to be used as the mainMoc. If your AppDelegate does not have an moc object, you must provide one that is created on the main queue.
Swift:
public var privateMoc: NSManagedObjectContext!Objective-C:
@property (readonly, strong, nonatomic) NSManagedObjectContext *privateMoc;Irrespective of the way the private moc is created, it is publicly exposed and you may use it, say to tie an NSFetchedResultsController to it and do asynchronous fetches.
Swift:
public var saveBlock: (() -> Void)?Objective-C:
@property (copy) SaveBlock saveBlock;A block that is fired when a save operation is completed.
Swift:
public func coreDataOperation()Objective-C:
- (void)coreDataOperation;This is the method you are required to override in your subclass. Any CoreData operations you wish to perform should be written here. The library will ensure that this method is called on the correct thread.
Usage
Swift:
var operationQueue = OperationQueue()
let operation = SomeCoreDataOperationSubclass(privateMoc: somePrivateMoc, mainMoc: nil)
operationQueue.addOperation(operation)Objective-C:
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
SomeCoreDataOperationSubclass *operation = [[SomeCoreDataOperationSubclass alloc] initWithPrivateMoc:somePrivateMoc mainMoc:nil];
[operationQueue addOperation:operation];As soon the operation is added to the operationQueue, it will start running on a background queue. You can use the completionBlock property to get the event when the operation finishes.
Credits
- To Shashank Pali for fixing the UI issues in the example project.
- To Manish Pathak for the motivation.
- Core Data Programming Guide - Concurrency
- Core Data from Scratch: Concurrency
- A Networked Core Data Application
- Common Background Practices
- Importing Large Data Sets
- iOS unrecognized selector sent to instance in Swift
To-do
A completion block to know when operation is complete.- A way to cancel operation midway.
License
ASJCoreDataOperation is available under the MIT license. See the LICENSE file for more info.