ReactiveCocoa and Core Data Concurrency

Recently, as discussed on some episodes of Build Phase, the iOS developers in San Francisco have finally gotten on the ReactiveCocoa bandwagon. While there are a lot of great resources to help you get up and running with the functional reactive programming paradigm, one thing that I didn’t feel like is well covered anywhere is using ReactiveCocoa in tandem with Core Data’s concurrency model. Ideally, we would use a private queue context to perform some intense work, then we would save that context and its changes would be merged into the main queue context via the NSManagedObjectContextDidSaveNotification notification. Following this pattern with ReactiveCocoa can produce some ugly code if you want to continue to transform the returned values. Let’s look at an example where we have a fetchObjects signal:

[[[[APIClient fetchObjects] map:^NSArray *(NSArray *objects) {
    __block NSArray *objects;
    [backgroundContext performBlockAndWait:^{
        pullRequests = [self createManagedObjectsFromJSON:objects
                                                inContext:backgroundContext];
    }];

    return objects;
}] map:^NSArray *(NSArray *objects) {
    __block NSArray *importantObjects;
    [backgroundContext performBlockAndWait:^{
        importantObjects = [objects filteredArrayUsingPredicate:
                            [self importantPredicate]];
    }];

    return importantObjects;
}] filter:^BOOL(NSArray *objects) {
    return objects.count > 0;
}];

The problem with this code, besides performBlockAndWait: possibly creating a deadlock, is Core Data doesn’t fit very well with ReactiveCocoa’s approach to signal flow. One way we can solve this is by executing our signals on a custom RACScheduler. This allows us to execute blocks on the private queue created by our NSManagedObjectContext. Let’s look at the subclass we need for this.

@interface CoreDataScheduler ()

@property (nonatomic) NSManagedObjectContext *context;

@end

@implementation CoreDataScheduler

- (instancetype)initWithContext:(NSManagedObjectContext *)context
{
    self = [super init];
    if (!self) return nil;

    self.context = context;

    return self;
}

- (RACDisposable *)schedule:(void (^)(void))block
{
    NSParameterAssert(block);

    RACDisposable *disposable = [RACDisposable new];

    [self.context performBlock:^{
        if (disposable.disposed) {
            return;
        }

        block();
    }];

    return disposable;
}

@end

First we’ll initialize our scheduler with a context. Then, in our schedule: method, we’ll perform our work in our context’s performBlock: method so it happens on the correct queue. Using this new scheduler we can clean up our original example.

CoreDataScheduler *scheduler = [[CoreDataScheduler alloc] initWithContext:context];
[[[[[[APIClient fetchObjects] deliverOn:scheduler] map:^NSArray *(NSArray *objects) {
    return [self createManagedObjectsFromJSON:objects inContext:context];
}] deliverOn:scheduler] map:^NSArray *(NSArray *objects) {
    return [objects filteredArrayUsingPredicate:[self importantPredicate]];
}] filter:^BOOL(NSArray *objects) {
    return objects.count > 0;
}];

Using deliverOn:, we can make our ReactiveCocoa blocks happen through our custom scheduler. This way we don’t have to worry about using performBlockAndWait: with Core Data and we end up with cleaner looking code. While using ReactiveCocoa with Apple’s frameworks we have found many places where modifications like this can greatly decrease the friction between programming paradigms.

More on learning ReactiveCocoa