Wrapping AFNetworking With ReactiveCocoa

When you learn to use ReactiveCocoa and discover how it can make your iOS apps more robust and more fun to develop and maintain then you suddenly want to use it all the time. Often it is necessary to bridge the Functional Reactive Programming (FRP) world with the imperative one. The cocoa touch frameworks and most third party libraries are based on imperative concepts and associated design patterns such as delegates, target-action and callback blocks. In FRP we like to operate on signals representing values changing over time instead of keeping track of state. Often it is worth the while to build a ReactiveCocoa bridge on top of existing APIs in order to consume them in a functional way.

One third party library used by a lot of apps is AFNetworking from Mattt Thompson. The latest version 2.0 has many features and offer many conveniences over using NSURLConnection directly. The AFNetworking API is also well documented.

If we write a few categories to offer a RACSignal-based API for the most used methods it will be easy to integrate with the rest of our ReactiveCocoa-based app.

AFHTTPRequestOperationManager is a class that makes it easy to dispatch network requests. One way to make a request is using the -[AFHTTPRequestOperationManager GET:parameters:success:failure] method. The full signature of the method is

1
2
3
4
- (AFHTTPRequestOperation *)GET:(NSString *)URLString
                     parameters:(NSDictionary *)parameters
                        success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
                        failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;

By creating a category on AFHTTPRequestOperationManager we can get an interface like this:

1
2
3
4
5
@interface AFHTTPRequestOperationManager (ReactiveExtension)

// sends a next with the AFHTTPRequestOperation instance and completes if the request succeeds.
- (RACSignal *)signalForGET:(NSString *)URLString parameters:(NSDictionary *)parameters;
@end

The method returns a signal that will be used to communicate the results of the request after it succeeds or fails. My implementation with ReactiveCocoa looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@implementation AFHTTPRequestOperationManager (ReactiveExtension)

- (RACSignal *)signalForGET:(NSString *)URLString parameters:(NSDictionary *)parameters {
  return [[RACSignal createSignal:^RACDisposable *(id <RACSubscriber> subscriber) {
      AFHTTPRequestOperation *op = [self GET:URLString parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) {
          [subscriber sendNext:operation];
          [subscriber sendCompleted];
      } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
          NSMutableDictionary *userInfo = [error.userInfo mutableCopy] ?: [NSMutableDictionary dictionary];
          userInfo[kAFNetworkingReactiveExtensionErrorInfoOperationKey] = operation;
          NSError *errorWithOperation = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo];
          [subscriber sendError:errorWithOperation];
      }];
      return [RACDisposable disposableWithBlock:^{
          [op cancel];
      }];
  }] replayLazily];
}

@end

There are a few things to note here. First the method creates a new signal using the +[RACSignal createSignal:] method. -[AFHTTPRequestOperationManager GET:parameters:success:failure]’s success block sends a next with the operation object and also completes the signal. The failure block is slightly more complicated. I wrap the AFHTTPRequestOperation instance in the user dictionary of the NSError object. This is useful in cases when the consumer needs to know the exact response data even though the request failed.

Another important detail is the RACDisposable object returned in the createSignal: method. The disposable is used to cancel the request operation in the case the subscriber disposes its subscription. This is a great example of the power of Reactive Cocoa. The request signal may be operated on by many parts of the app and the object eventually subscribing to it may not need to know about the internals of the network layer. Still it can cancel the request if it does not need the response anyway.

Finally I apply the replayLaizily operator on the signal to ensure side effects only occur once even if the signal is subscribed to multiple times.

UPDATE: Turns out there is already a pod that wraps all AFNetwork’s block based methods called AFNetworking-RACExtensions. Unfortunately the original repo and podspec has not been updated for AFNetworking 2.0, but there is a fork that seems to work well. Just put this in your Podfile to use the fixed fork:

1
pod 'AFNetworking-RACExtensions', :git => 'https://github.com/knshiro/AFNetworking-RACExtensions.git'

When you have the AFNetworking-RACExtensions pod you can go ahead and make requests like:

1
2
3
4
5
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[[manager rac_GET:@"http://exampl.com/1.json" parameters:nil] subscribeNext:^(RACTuple *tuple) {
  RACTupleUnpack(AFHTTPRequestOperation *operation, NSDictionary *response) = tuple;
  NSLog(@"response: %@", [response description]);
}];

Of course the real fun doesn’t start until you begin operating on and combining signals, but that’s a topic for another blog post.

Comments