stream

So recently I’ve got tired of writing massive nested callback blocks in Objective-C (like what they called “callback hell” in JS community), and I decided to give reactive programming in Cocoa a try.

I’ve written quite some Node.js in the past few months both in my summer intern and for my personal projects, and, due to JavaScript’s async nature, the callback hell problem is way worse than that in Objective-C. To solve this I learned and used PromiseJS, a js library designed to address this exact problem. I really enjoyed the idea of creating Promise from async function and then get the eventual value later. It produces much more readable and maintainable code, with less potential bugs.

To do this in Objective-C I looked into two frameworks: PromiseKit and ReactiveCocoa. I considered PromiseKit first since I’ve been familiar with the promise in js. For ReactiveCocoa, I’ve used OctoKit.objc before in my projects, and I’ve contributed a small method to it. When I read through their source code I was impressed by how they use AFNetworking in combo with ReactiveCocoa. This makes me want to try out ReactiveCocoa too.

Eventually I decided to go with ReactiveCocoa instead of PromiseKit, because PromiseKit has only 2 built-in Networking libraries: Alamofire and OMGHTTPURLRQ, but not AFNetworking that I heavily use. Of course you can write a bunch of categories yourself to make AFNetworking to work with PromiseKit, but that requires extra work. There are libraries on GitHub that do this for you (like this one recommended officially by PromiseKit), but I haven’t find a decent one that supports AFNetworking 3.

Also, as discussed here, PromiseKit provides only a small feature from reactive programming, while libraries like rxSwift/ReactiveCocoa are much purer ways to go reactive in iOS.

For example, with ReactiveCocoa, you can eliminate the use of delegates in most cases (they are still used behind the scenes, of course, but we write better and clearer code by replacing them with RACSignal in ReactiveObjC). Don’t get me wrong, I am not saying delegate pattern is bad. It’s good for complex structures like UITableViewController where a huge number of delegate methods are provided in UITableViewDelegate and UITableViewDataSouece. But for most cases where all a delegate method does is simply tells you when something is done, using block syntax or ARCSignal in ReactiveCocoa are much better choices.

Here’s an example: in one of my projects, I used CoreLocation and need the two delegate methods locationManager:didUpdateLocations: and locationManager:didFailWithError: from CLLocationManagerDelegate. We can wrap these two delegate method into RACSignal like so:

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
29
30
31
32
33
#import "CLLocationManager+RAC.h"
@implementation CLLocationManager(RAC)
- (RACSignal *)RACLocationUpdated {
self.delegate = self;
RACSignal *signal = objc_getAssociatedObject(self, _cmd);
if (signal) return signal;
signal = [[self rac_signalForSelector:@selector(locationManager:didUpdateLocations:) fromProtocol:@protocol(CLLocationManagerDelegate)] map:^id(RACTuple *tuple) {
return tuple.second;
}];
objc_setAssociatedObject(self, _cmd, signal, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return signal;
}
- (RACSignal *)RACError {
self.delegate = self;
RACSignal *signal = objc_getAssociatedObject(self, _cmd);
if (signal) return signal;
signal = [[self rac_signalForSelector:@selector(locationManager:didFailWithError:) fromProtocol:@protocol(CLLocationManagerDelegate)] map:^id(RACTuple *tuple) {
return tuple.second;
}];
objc_setAssociatedObject(self, _cmd, signal, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return signal;
}
@end

Then you can use them like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
_clManager = [CLLocationManager new];
_clManager.desiredAccuracy = kCLLocationAccuracyBest;
[_clManager requestWhenInUseAuthorization];
[_clManager startUpdatingLocation];
[[_clManager RACLocationUpdated] subscribeNext:^(NSArray *locations) {
NSLog(@"updated locations: %@", locations);
}];
[[_clManager RACError] subscribeNext:^(NSError *error) {
NSLog(@"failed with error: %@", error);
}];

As you can see RACSignal are much cleaner to handle that delegate methods. Obviously this cannot be done in PromiseKit, so ReactiveObjC seems to be a better choice for me. In fact ReactiveCocoa has already wrapped many delegate methods in UIKit for you. For example, [aTextField rac_textSignal] returns a signal for the text in aTextField whenever its text changes.

Also, for networking, you can use ReactiveCocoa with AFNetworking like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
+(RACSignal *)RACGetJsonFromUrl:(NSString *)url parameters:(NSDictionary *)parameters {
return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
[manager.requestSerializer setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData];
manager.responseSerializer = [AFJSONResponseSerializer serializer];
[manager GET:url parameters:parameters progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
if (responseObject){
[subscriber sendNext:responseObject];
[subscriber sendCompleted];
}
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
[subscriber sendError:error];
}];
return nil;
}];
}

Note:

You may wonder why I chose ReactiveCocoa over rxSwift. Well yes both of them are reactive programming libraries for Cocoa, but ReactiveCocoa has a longer history and has been battle tested in the official GitHub client for mac. Also, ReactiveCocoa has a pure Objective-C version: ReactiveObjC, so I can use it in Objective-C projects without worrying about the future breaking changes in Swift would break it. For more comparison between ReactiveCocoa and rxSwift, you can read this post from Ray Wenderlich.