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.