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:
1 |
NSString* myFileContents = [NSString stringWithContentsOfFile:myPath encoding:NSUTF8StringEncoding error:nil];
|
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.
Delegates
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):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
-(void) someMethod {
// ...
[NSURLConnection connectionWithRequest:urlRequest delegate:self];
//
}
// delegate callbacks
-(NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse {
return nil;
}
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
}
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
// process the data here
}
-(NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse{
return request;
}
-(void)connectionDidFinishLoading:(NSURLConnection *)connection{
}
-(void)connection:(NSURLConnection *)connection didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite{
}
|
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.
Blocks
Blocks are a new feature to the Objective-C language since iOS 4, which was introduced in 2010. Blocks remind me of JavaScript’s anonymous functions, which are used for the same purpose.
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.
1
2
3
4
5
|
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
// process the data here
} failure:nil];
[operation start];
|
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.
Nested Blocks
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
NSURLRequest *request1 = [NSURLRequest requestWithURL:url]; // first chunk of code processed
AFJSONRequestOperation *operation1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:request1 success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
// the second chunk of code run
// process some data
NSURLRequest *request2 = [NSURLRequest requestWithURL:url2];
AFJSONRequestOperation *operation2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:request2 success:^(NSURLRequest *req, NSHTTPURLResponse *response, id JSON) {
// here's the third chunk of code run
// process more data
} failure:nil];
[operation2 start]; // still running the second chunk of code
} failure:nil];
[operation1 start]; // still running as the first chunk of code
|
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
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#import <ReactiveCocoa/ReactiveCocoa.h>
// ...
-(RACSignal*)signalGetNetworkStep1 {
NSURL* url = ...;
return [RACSignal createSignal:^(RACDisposable *(id subscriber) {
NSURLRequest *request1 = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
[subscriber sendNext:JSON];
[subscriber sendCompleted];
} failure:^(AFHTTPRequestOperation *operation, NSError *error){
[subscriber sendError:error];
}];
[operation1 start];
return nil;
}
}
|
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.
1
2
3
|
[[self signalGetNetworkStep1] subscribeNext:^(id *x) {
// perform your custom business logic
}];
|
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
-(RACSignal*)signalGetNetworkStep2:(id inputJSON) {
// do something with inputJSON to create the url for this method
NSURL* url = ...;
return [RACSignal createSignal:^(RACDisposable *(id subscriber) {
NSURLRequest *request2 = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
[subscriber sendNext:JSON];
[subscriber sendCompleted];
} failure:^(AFHTTPRequestOperation *operation, NSError *error){
[subscriber sendError:error];
}];
[operation2 start];
return nil;
}
}
|
The signal is similar to the last but it takes input from the last signal. Here’s how the two signals are chained together.
1
2
3
4
5
6
7
|
__weak id weakSelf = self;
[[[self signalGetNetworkStep1] flattenMap:^RACStream*(id *x) {
// perform your custom business logic
return [weakSelf signalGetNetworkStep2:x];
}] subscribeNext:^(id *y) {
// perform additional business logic
}];
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
__weak id weakSelf = self;
[[[[[self signalGetNetworkStep1] flattenMap:^RACStream*(id *x) {
// perform your custom business logic
return [weakSelf signalGetNetworkStep2:x];
}] flattenMap:^RACStream*(id *y) {
// perform additional business logic
return [weakSelf signalGetNetworkStep3:y];
}] flattenMap:^RACStream*(id *z) {
// more business logic
return [weakSelf signalGetNetworkStep4:z];
}] subscribeNext:^(id*w) {
// last business logic
}];
|
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.
Further Reading
The documentation in the GitHub repository is where I get most details. If I have specific questions, I’ll read StackOverflow.