Dealing with asynchronous operations is a common problem in mobile development. To keep our app’s user interface as fast and responsive as possible, we need to offload network requests, resource loading (e.g., images), bluetooth operations, and file I/O onto a background thread.
Calling one asynchronous routine and responding to its result is a relatively simple matter, but what happens when several different asynchronous operations must be executed in some set order (serialized)? Apple has built in support for queuing asynchronous operations with NSOperationQueue
and Grand Central Dispatch, but there’s another solution: using a functional reactive approach with Reactive Cocoa.
ReactiveCocoa’s RACSignal Class
ReactiveCocoa is an Object-C framework for functional reactive programming — a combination of functional aspects (telling the computer what to do not how to do it, passing functions and blocks as objects, etc.) and reactive programming (focuses on reacting to data changes, data flows, etc.).
To this end, ReactiveCocoa provides a RACSignal
class that captures future and present values and may be chained together. RACSignal
can send and receive (using subscription) data using three possible messages: next
, error
, and complete
. next
passes along some object in the stream of data, while error
and complete
tell the subscriber that the signal is finished sending data (an error occurred or it has completed successfully).
Here’s a very simple example from the ReactiveCocoa github documentation:
// When self.username changes, logs the new name to the console.
//
// RACObserve(self, username) creates a new RACSignal that sends the current
// value of self.username, then the new value whenever it changes.
// -subscribeNext: will execute the block whenever the signal sends a value.
[RACObserve(self, username) subscribeNext:^(NSString *newName) {
NSLog(@"%@", newName);
}];
The key concept here is one can subscribe to a RACSignal
and then react when data is sent. This behavior is what makes functional reactive programming perfect for handling asynchronous operations.
The Asynchronous Chain Pattern
I’ve recently been working on an iOS project that needs to periodically communicate with a Bluetooth Low Energy (LE) device. In order to save device battery power, we need to connect/disconnect before and after each communication. Additionally, some of the work we need to do consists of several atomic operations within the connect/disconnect.
Each step (connection, disconnection, and the operations in between) is asynchronous — subject to errors, timeouts, and the capricious nature of Core Bluetooth. Furthermore, if any step in the chain fails, we should cancel the future work in the chain, disconnect, and report an error.
The chain of operations looks like this:
Fortunately, implementing this pattern is something ReactiveCocoa excels at. Here’s the code:
[[[[service connect] flattenMap:^RACStream *(id value) {
return [service doSomething1];
}] flattenMap:^RACStream *(id something1Value) {
// if doSomething1 is successful, 'somethingValue' is passed via sendNext
return [service disconnect];
}] subscribeError:^(NSError *error) {
// Error occurred! Handle "error" if necessary.
} completed:^{
// Asynchronous chain of operations succeeded.
}];
Whew. There’s a lot going on in the above chunk of code, so let’s work through it one bit at a time.
1. service connect, disconnect, and doSomething1
Each of these calls are methods on the object instance service
. (In this example service
is simply a class we wrote containing the API for our device, implementing NSObject.) They all return a RACSignal
.
The following code gives an example of what connect
might look like. We assume that service
uses an external API, externalService
, that has a more traditional completion/error/timeout block based interface. (disconnect
and doSomething1
, doSomething2
, etc. will have similar implementations.)
-(RACSignal *)connect {
return [RACSignal startLazilyWithScheduler:[RACScheduler schedulerWithPriority:RACSchedulerPriorityBackground] block:^(id subscriber) {
[externalService connectWithSuccess:^void() {
// Connection succeeded, pass nil (or some useful information) along and complete
[subscriber sendNext:nil];
[subscriber sendCompleted];
} errorOrTimeout:^void() {
// Error occurred, pass it along to the subscriber
[subscriber sendError:someNSError];
}];
}];
}
When a subscriber subscribes to to the RACSignal
returned by connect (via, for example, subscribeNext
), the block of contained in startLazilyWithScheduler
is executed on a background RACScheduler
. The block is passed a subscriber object, which can then be signaled next
, complete
or error
when the corresponding completion/error blocks are called via the old externalService
API. Note that we complete
after sending next
because the connection is successful and the signal will not be used anymore. Completion (and error) tell ReactiveCocoa to clean up the signal.
To summarize, [service connect]
immediately returns a RACSignal
, which is then subscribed to. When subscription occurs, the block contained in connect’s startLazilyWithScheduler
executes, and eventually errors or succeeds.
2. flattenMap and return [service doSomething1]
Those familiar with functional programming will recognize map
, which simply maps one value to another value. (The value could be a signal.) ReactiveCocoa also provides a flattenMap
block, which requires the caller to map a value to a signal.
In our case, we’re taking the return type of [service connect]
— a RACSignal
— and mapping it to another signal: [service doSomething1]
. The flattenMap
block will only be called when next
is sent from the signal returned by [service connect]
. (Recall in the above section that connect will sendNext inside its success block.) This means that when [service connect]
succeeds, the flattenMap
block is run, which maps to a new signal, the return value of [service doSomething1]
, which is subsequently flattenMapped into [service disconnect]
.
So we have a chain of three signals, and two mappings (from [service connect]
to [service doSoemthing1]
, and from [service doSomething1]
to [service disconnect]
). And the aggregate of these signals/mappings is kicked off by the final bit of code in the block: subscribeError:completed
.
3. subscribeError:completed
The chain of three operations is started when we subscribe to it via subscribeError:completed
. Specifically, the first startLazily block in connect
is called and waits for a response from the externalService
. If it succeeds, sendNext
is called, and the flattenMap
block is executed, starting the next operation in the chain. If everything works, the completed block will be called (because we sendCompleted in each service api method).
But, what if there’s an error? Good news — in ReactiveCocoa errors, are propagated immediately and have exception semantics. That means an error sent at any point in the chain of operations terminates the chain and causes the subscribeError
block to be called. Any NSError
object created within the service calls will be propagated up to the subscribe at the end. Super convenient.
Serializing the Asynchronous Chain
The asynchronous chain pattern I described helped my team create arbitrary combinations of operations, but we found we had another problem. During stress testing, it became readily apparent that two chains of operations could become interspersed and race conditions could occur — especially when the asynchronous operations could be initiated by the user at any time (through the UI). To keep things simple, we decided to serialize the chains of operations on a single RACScheduler and make each chain blocking:
To implement this pattern, we simply create a RACScheduler
(similar to a GCD dispatch_queue
) and schedule the chain on it — blocking to ensure no other chain of operations can start until the current chain is finished. To block, we call the firstOrDefault:success:error
method on RACSignal
, which allows us to get the final object sent along by the last operation in the chain, or an error and default value.
RACScheduler *serializedScheduler = [RACScheduler schedulerWithPriority:RACSchedulerPriorityDefault];
[serializedScheduler schedule:^{
BOOL success;
NSError *error;
id finalValue = [[[[service connect] flattenMap:^RACStream *(id value) {
return [service doSomething1];
}] flattenMap:^RACStream *(id something1Value) {
// [service doSomething1], if successful passes along 'somethingValue'
return [service disconnect];
}] firstOrDefault:defaultValue success:&success error:&error];
if (success) {
// Operation succeeded! The last value that was returned via sendNext is stored in finalValue,
// success is YES, and error will be nil.
}
else {
// Operation failed! finalValue will be set to defaultValue, success is NO, and error will be non-nil
// if an NSError was propagated up by the chain of operations.
}
}];
Conclusion
My team’s original callback-based bluetooth implementation had problems with maintaining state, avoiding race conditions, and high complexity. ReactiveCocoa helped us make sense of the problem in a functional and expressive way.
The learning curve for ReactiveCocoa is one of the steepest I have personally encountered, but the following links helped me understand this pattern better:
- Ray Wenderlich’s awesome Reactive Cocoa Tutorial (which covers this asynchronous pattern, and flattenMap as well!).
- This github thread discussing the differences between
map
andflattenMap