When writing a software application, it is often necessary to read data from a file or network. The basic approach for reading data is performed as a blocking operation. A blocking operation waits until all data is retrieved before performing the next instruction. Here’s an example of reading a file in Objective-C:
Software developers often start with blocking calls because most APIs were originally designed that way and it’s simple to understand. When reading small files from disk on a smartphone or computer, blocking time is almost negligible and probably isn’t even noticed by the user.
In contrast, reading data over a network connection can be extremely slow. Especially on smartphones, cellular network strength degrades when going around buildings and inside tunnels. As a result, when a network request is made and the network drops, the blocking call may result in a frozen user interface. In the best case scenario, the app is sluggish to respond when network access is required.
The problem of slow blocking operations is solved through asynchronous callbacks in Objective-C. The basic pattern is to initiate an operation on the main thread and retrieve data on a separate thread. When the data becomes available, a callback is run on the main thread. These asynchronous callbacks can be implemented as delegates or blocks.
Asynchronous callbacks can be implemented using delegates. Delegates are objects that conform to a given protocol, i.e., implement a set of required method signatures. Here’s an Objective-C file that conforms to NSURLConnectionDelegate (example based on iOS 4.3):
This approach has several advantages for software developers:
- It is the primary way that UI events are implemented in Cocoa and CocoaTouch.
- It is simple to understand: implement these methods and the asynchronous callback will perform at a later time.
- Developers don’t have to deal with weird retain/release/ARC issues.
The major downside to delegates occurs when multiple asynchronous operations are issued from the same file and have the same delegate. Conditional statements are required in the delegate function to determine the caller. That’s when we take another approach: blocks.
Here’s an example of making a network request using a nested block. I’m going to use AFNetworking for this example because it wraps core functionality of NSURLConnection.
The second line declares the asynchronous callback block but the code inside it haven’t run yet. The last line, [operation start], initiates the call on another thread. When the call completes, the success block is called.
Aside: As an alternative to AFNetworking and NSURLConnection blocks, I’ve seen some developers use GCD to call a synchronous network operation in a background GCD queue and then process its callback in the main GCD queue.
When writing a real-world application with multiple network calls, it would be convenient to have them called in sequence. Usually the output of one call is the input of the next call. We can implement a sequence of asynchronous operations as:
Observe how one callback block is nested inside another. The more asynchronous network calls we have, the more nesting of callback blocks.
Also notice how it’s getting harder to read. Code is no longer read top-down. A developer has to be cognizant of the order of execution. I’ve highlighted which chunk of code gets run first, second, and third. In order to deal with growing asynchronous callback sequences, ReactiveCocoa introduces a design pattern to keep code clean.
ReactiveCocoa is based on functional reactive programming (FRP), which is a design pattern that enables a developer to read asynchronous callbacks completing top-down. Several useful tutorials are available on the Internet for learning FRP. I’m going to describe how I currently understand ReactiveCocoa.
The core concept to ReactiveCocoa are streams. Streams include both signals and sequences. Signals are push streams, e.g., network callbacks. Sequences are pull streams, e.g., lazy-evaluated lists. I conceptualize streams as asynchronously retrieved data.
A Single Signal
Here’s an example signal that reads network data.
A RACSignal should wrap an operation with an asynchronous callback block. The outer method is useful for organizing code, as you will see next.
Inside the callback block, the method send three types of signals to subscriber: next, success, and error. Next signal is called to provide data for your own business logic, which is handled somewhere else. Completed stops this signal immediately. Error also stops the signal immediately and prevents any other signal in the chain from running.
Now let’s see how a signal is used.
The signal is run and the asynchronous callback logic is preformed within this block. For a single asynchronous callback, this is overkill.
Several Signals in Sequence
The beauty of ReactiveCocoa is seen when several asynchronous callbacks are required. Let’s introduce an additional call.
The signal is similar to the last but it takes input from the last signal. Here’s how the two signals are chained together.
A couple of things changed. The last signal to run should call subscribeNext for receiving output values. If no output values are available, subscribeNext can be replaced by subscribeCompleted or subscribeError. Read the header file for details.
Any intermediate step is defined by flattenMap. The parameter in flattenMap corresponds to the data from the next signal. It also takes a return value, which is the subsequent signal to run. The two signals are written top-down and are executed top-down.
There are some oddities. We don’t want to strongly retain self: in ARC, we introduce __weak. Also, the extra square brackets at start and end are ugly in Objective-C’s implementation of FRP.
If you increase the asynchronous callback chain longer, it remains as simple as shown above. With 4 network steps, this asynchronous callback chain would look like:
Parallel Signal Execution
Another awesome feature advertised in ReactiveCocoa is to issue several asynchronous operations in parallel and then merge the results together. I haven’t tried it but will write about it when I do. Read the documentation for details.