GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS

Written by thoughtbot

A Guide to Core Data Concurrency

The iOS ethos of instant responsive UI elements means putting as much work as possible in background threads and as little work in the main thread. For most cases we are fine with using an NSOperationQueue or GCD, but getting concurrency to work in Core Data sometimes feels more like black magic than science. This post intends to demystify concurrency and offer two ways to go about it.

Setup 1: Private queue context and main queue context off of single persistent store coordinator

In this setup we will make two NSManagedObjectContext instances one with concurrency type NSMainQueueConcurrencyType and the other with type NSPrivateQueueConcurrencyType. We will attach ourself to the NSManagedObjectContextDidSaveNotification to propagate saves. The power of this setup is simplicty. Use this setup on applications which do not require running core data on operation queues. This stack is ideal for an application which needs a context to do some background work and will mostly be using the main queue context to display information.

We add two methods to our TBCoreDataStore.h file:

+ (NSManagedObjectContext *)mainQueueContext;
+ (NSManagedObjectContext *)privateQueueContext;

In our implementation file we add two private properties and lazy load them:

@interface TBCoreDataStore ()

@property (strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;
@property (strong, nonatomic) NSManagedObjectModel *managedObjectModel;

@property (strong, nonatomic) NSManagedObjectContext *mainQueueContext;
@property (strong, nonatomic) NSManagedObjectContext *privateQueueContext;

@end

#pragma mark - Singleton Access

+ (NSManagedObjectContext *)mainQueueContext
{
    return [[self defaultStore] mainQueueContext];
}

+ (NSManagedObjectContext *)privateQueueContext
{
    return [[self defaultStore] privateQueueContext];
}

#pragma mark - Getters

- (NSManagedObjectContext *)mainQueueContext
{
    if (!_mainQueueContext) {
        _mainQueueContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
        _mainQueueContext.persistentStoreCoordinator = self.persistentStoreCoordinator;
    }

    return _mainQueueContext;
}

- (NSManagedObjectContext *)privateQueueContext
{
    if (!_privateQueueContext) {
        _privateQueueContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        _privateQueueContext.persistentStoreCoordinator = self.persistentStoreCoordinator;
    }

    return _privateQueueContext;
}

Next we override the initializer to add our observing:

- (id)init
{
    self = [super init];
    if (self) {
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(contextDidSavePrivateQueueContext:)
                                                     name:NSManagedObjectContextDidSaveNotification
                                                   object:[self privateQueueContext]];
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(contextDidSaveMainQueueContext:)
                                                     name:NSManagedObjectContextDidSaveNotification
                                                   object:[self mainQueueContext]];
    }
    return self;
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)contextDidSavePrivateQueueContext:(NSNotification *)notification
{
    @synchronized(self) {
        [self.mainQueueContext performBlock:^{
            [self.mainQueueContext mergeChangesFromContextDidSaveNotification:notification];
        }];
    }
}

- (void)contextDidSaveMainQueueContext:(NSNotification *)notification
{
    @synchronized(self) {
        [self.privateQueueContext performBlock:^{
            [self.privateQueueContext mergeChangesFromContextDidSaveNotification:notification];
        }];
    }
}

Now we have a working private queue and main queue context which will both be updated whenever one is saved. Here is an example usage:

[[TBCoreDataStore privateQueueContext] performBlock:^{
    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"MyEntity"];
    NSArray *results = [[TBCoreDataStore privateQueueContext] executeFetchRequest:fetchRequest
                                                                            error:nil];
}];

One of the great advantages of this type of core data stack is it allows us to make great use of NSFetchedResultsController. An example of this is parsing JSON from a web service into a core data object as a background operation and then using the fetched results controller to indicate when said object has changed and updating the UI as a result. For simple applications this stack is headache free and flexible.

Setup 2: The throwaway main queue context backed by a private queue context.

In this setup we will have only one NSManagedObjectContext which will stay with us for the life time of the app. This will be a private queue context which we will use to create child main queue contexts from. This allows us to spend as much time in the background and only when we need to do UI work do we create a new main queue context. This is a slightly more complex stack, and should be used when your application needs to run core data on multiple background threads. This setup relies on having a single private queue context which will serve as a parent to freshly made contexts. Use this stack when you want to run core data on an operation queue.

Starting from our base core data setup we add the following to TBCoreDataStore.h:

+ (NSManagedObjectContext *)newMainQueueContext;
+ (NSManagedObjectContext *)newPrivateQueueContext;
+ (NSManagedObjectContext *)defaultPrivateQueueContext;

In our implementation file we add a single property and lazy load it:

@interface TBCoreDataStore ()

@property (strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;
@property (strong, nonatomic) NSManagedObjectModel *managedObjectModel;

@property (strong, nonatomic) NSManagedObjectContext *defaultPrivateQueueContext;

@end

#pragma mark - Singleton Access

+ (NSManagedObjectContext *)newMainQueueContext
{
    NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    context.parentContext = [self defaultPrivateQueueContext];

    return context;
}

+ (NSManagedObjectContext *)newPrivateQueueContext
{
    NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    context.parentContext = [self defaultPrivateQueueContext];

    return context;
}

+ (NSManagedObjectContext *)defaultPrivateQueueContext
{
    return [[self defaultStore] defaultPrivateQueueContext];
}

#pragma mark - Getters

- (NSManagedObjectContext *)defaultPrivateQueueContext
{
    if (!_defaultPrivateQueueContext) {
        _defaultPrivateQueueContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        _defaultPrivateQueueContext.persistentStoreCoordinator = self.persistentStoreCoordinator;
    }

    return _defaultPrivateQueueContext;
}

Here we have no need to observe save notifications as any saves on the created main queue context will bubble up to its parent the defaultPrivateQueueContext. This approach is very robust and spends the least possible time on the main queue. The downside is that it we cannot use the NSFetchedResultsController out of the box though we could cobble our own version using the different notifications sent out by core data. The power of this stack is its flexibility. You can spin up new managed object contexts safely inside an NSOperation subclass to do some heavy lifting or you have the option of just calling the defaultPrivateQueueContext. Use this setup if you need full control over core data.

Lets say we have a really big database (20k+ objects) and we want to do a complex fetch, the best way to go about this is to first use the background queue to fetch the NSManagedObjectIDs and then hop on the main queue and call -objectWithID on the results. This is how we should always be passing around managed objects between threads.

NSManagedObjectContext *workerContext = [TBCoreDataStore newPrivateQueueContext];
[workerContext performBlock:^{

    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"MyEntity"];
    fetchRequest.resultType = NSManagedObjectIDResultType;

    NSArray *managedObjectIDs = [workerContext executeFetchRequest:fetchRequest error:nil];

    NSManagedObjectContext *mainQueueContext = [TBCoreDataStore newMainQueueContext];
    [mainQueueContext performBlock:^{

        for (NSManagedObjectID *managedObjectID in managedObjectIDs) {
            MyEntity *myEntity = [mainQueueContext objectWithID:managedObjectID];
            // Update UI with myEntity
        }
    }];
}];

In this scenario we need to update our UI with a bunch of MyEntity managed objects. For efficiencie’s sake we perform the costly fetch in the background and set the result type to be NSManagedObjectIDResultType which will return NSManagedObjectIDs. We then create a new mainQueueContext and get each managed object from the cache by using [mainQueueContext objectWithID:managedObjectID]. These objects are then safe to use on the main thread.

If your fetch is not too intensive we can just perform it on your new main queue context. If we want to use this stack I recommend making this snippet:

NSManagedObjectContext *mainQueueContext = [TBCoreDataStore newMainQueueContext];
[mainQueueContext performBlock:^{
    <#code#>
}];

Caveats

When performing extremely intensive fetch operations (10+ seconds) in the background thread and simultaneously needing to perform operations on another thread we will run into blockage. To prevent this from happening we should perform this operation an an entirely new context linked to a entirely new PSC. This will ensure that the operation stays in the background.

Useful utility methods

An extremely useful little one liner is the ability to turn an NSManagedObjectID into a string. we can use this to store the ID in the user defaults.

@implementation NSManagedObjectID (TBExtras)

- (NSString *)stringRepresentation
{
    return [[self URIRepresentation] absoluteString];
}

The flip side of this is then to get an NSManagedObjectID out of such a string. Add this method to your CoreDataStore:

+ (NSManagedObjectID *)managedObjectIDFromString:(NSString *)managedObjectIDString
{
    return [[[self defaultStore] persistentStoreCoordinator] managedObjectIDForURIRepresentation:[NSURL URLWithString:managedObjectIDString]];
}

With these two methods we have an easy way to build a cache on disk by using a plist. This is useful for saving a list of managed objects which need to be updated or maybe deleted between app launches.

Creating a managed object is a pain, so here is a little method which will make your life better:

@implementation NSManagedObject (TBAdditions)

+ (instancetype)createManagedObjectInContext:(NSManagedObjectContext *)context
{
    NSEntityDescription *entity = [NSEntityDescription entityForName:NSStringFromClass([self class])
                                              inManagedObjectContext:context];
    return  [[[self class] alloc] initWithEntity:entity
                  insertIntoManagedObjectContext:context];
}

Finally, while Apple does provide a method to get an NSManagedObject from an NSManagedObjectID we often want to convert a whole array of ids into objects to do this we can use the following:

@implementation NSManagedObjectContext (TBAdditions)

- (NSArray *)objectsWithObjectIDs:(NSArray *)objectIDs
{
    if (!objectIDs || objectIDs.count == 0) {
        return nil;
    }
    __block NSMutableArray *objects = [[NSMutableArray alloc] initWithCapacity:objectIDs.count];

    [self performBlockAndWait:^{
        for (NSManagedObjectID *objectID in objectIDs) {
            if ([objectID isKindOfClass:[NSNull class]]) {
                continue;
            }

            [objects addObject:[self objectWithID:objectID]];
        }
    }];

    return objects.copy;
}

@end

What’s next?

Lets recap our two core data stacks. We have setup 1, which is simple and provides you with the ability to perform background fetches and saves. While most applications will only require you to handle two contexts, a private queue and a main queue context, there are situations where being able to spawn contexts at will becomes important. This is where setup 2 comes in. With this setup you will be making new contexts which are children of a single parent private queue context. This setup shines when combined with NSOperation subclasses which need to communicate with core data.

I’ve placed the two core data stacks on GitHub.

Mark and Gordon talked about this on Build Phase episode 18.