ReactiveCocoa tutorial

What is ReactiveCocoa

ReactiveCocoa is an FRP framework for iOS, consisting of several components that let developers write code in a declarative manner rather than imperative. The main idea behind FRP is to represent program flow as sequences of events. With the help of powerful functional programming building blocks like values transforming, filters, sequences merging and concatenating, it provides a very flexible and convenient framework that lets developers write code that is much less error prone.

ReactiveCocoa is in fact a ReactiveExtensions port from .NET to objc, made by GitHub developers. You can find more information on ReactiveExtensions relevant to ReactiveCocoa on the official readme.

Installation

There are two ways you can integrate ReactiveCocoa into your project. The first is by adding the ReactiveCocoa repo as a submodule to your repo, then manually adding the framework to your project. This process is very well described on the official readme.

The second way is by using CocoaPods and is as simple as adding one new pod to your podfile.

pod 'ReactiveCocoa', '~>2'
pod 'libextobjc'

In this example, I specify that I want to use the latest ReactiveCocoa version 2.x. I'd like to specify the version explicitly here as I know the upcoming version 3 is about to appear soon and it'll be API-incompatible with the existing one, so in order to protect ourselves for the future it's better lock the version here. Another pod I added is libextobjc. It's not crucial, but it provides some very handy functionality used widely when dealing with ReactiveCocoa, specifically its EXTScope module.

Basic Usage Concepts

As I mentioned, FRP represents everything as sequences of events or values. This is a very powerful abstraction. Any current value and possible future values for the same object are represented as signals, backed by a RACSignal class cluster. There are several ways of creating signals: by observing KVO-compatible properties, special additions to UIKit generating signals for UIKit-generated events, RACSubject, or dynamic signals.

You can find more detailed information on how to create these signals in the http://rcdp.io/ project in this post. For now, I'd like to show you two basic ways of signals creation:

// KVO based observing
RACSignal *kvoPropertySignal = RACObserve(object, property);

 // dynamic signal with async action with block-based callback
 RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    id operation = asynchronusAction(^(id asyncActionResult, NSError *error) {
        if (error) {
            [subscriber sendError:error];
        }
        else {
            [subscriber sendNext:asyncActionResult];
            [subscriber sendCompleted];
        }
    });
    return [RACDisposable disposableWithBlock:^{
        [operation cancel];
    }];
}];

Once you have a signal, you can transform values passed along the signal with -map:, filter some values with -filter:, mix signals together in several ways: -merge:, -concat:, combine with -combineLatest: and so on. You can see all the possible operations available for signals in the official docs. It's very detailed and easy to understand.

I recommend using MVVM pattern for organizing the model-view interaction. The main idea is that viewModel takes the model object as input and generates several outputs as needed by the UI. Then, in the ViewController you can glue together the UI and the viewModel by using RAC and RACObserve macros. Let's take a closer view at a particular example.

Example

Let's make a project consisting of one TableView representing the first page of the response of API call to https://api.github.com/users (see docs for this API here). For simplicity, I will skip the boring setting up stuff, assuming you as an experienced developer are comfortable with that.

The project should consist of one UITableViewController (I call it SRGViewController) with one simple UITableViewCell prototype containing UIImageView for the user's userpic and a label for the user's name. Let's create a class for this custom cell called SRGTableViewCell.

Next, for both these components we create ViewModels: SRGViewModel for tableViewController and SRGCellViewModel for TableviewCell. The first is responsible for making the API call and holds the user's objects array, while the second represents the data for this view for this particular user. Here's what these viewModels look like:

@implementation SRGViewModel
- (instancetype)init {
    self = [super init];
    if (self) {
        RAC(self, users) = [[[NSURLConnection rac_sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://api.github.com/users"]]]
                            map: ^id (RACTuple *value) {
            NSData *data = value.second;
            NSError *error = nil;
            NSArray *array = [NSJSONSerialization JSONObjectWithData:data
                                                             options:0
                                                               error:&error];
            return array;
        }] deliverOn:RACScheduler.mainThreadScheduler];
    }
    return self;
}


@implementation SRGCellViewModel

- (instancetype)init {
    self = [super init];
    if (self) {
        RAC(self, username) = [RACObserve(self, userModel) map: ^id (NSDictionary *value) {
            return value[@"login"];
        }];
        RAC(self, image) = [[[RACObserve(self, userModel) skip:1] map: ^id (NSDictionary *value) {
            return [[[NSURLConnection rac_sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:value[@"avatar_url"]]]]
                     map: ^id (RACTuple *value) {
                return [UIImage imageWithData:value.second scale:[UIScreen mainScreen].scale];
            }] deliverOn:RACScheduler.mainThreadScheduler];
        }] switchToLatest];
    }
    return self;
}

Hooking up these viewModels in the controller's code is as simple as this:

@implementation SRGViewController

- (void)awakeFromNib {
    @weakify(self)
    self.viewModel = [[SRGViewModel alloc] init];
    [[RACObserve(self, viewModel.users) ignore:nil] subscribeNext : ^(id x) {
        @strongify(self)
        [self.tableView reloadData];
    }];
}
...
@end

@implementation SRGTableViewCell

- (void)awakeFromNib {
    self.viewModel = [SRGCellViewModel new];
    RAC(self, userpicImageView.image) = RACObserve(self, viewModel.image);
    RAC(self, usernameLabel.text) = RACObserve(self, viewModel.username);
}

@end

That's all the code! Youi'll find the full project source on github.

For more information about the practical usage of ReactiveCocoa, an explanation of the framework components and some complete design patterns, you can see my RAC-related blog http://rcdp.io.

0 comments


Or enter your name and Email
No comments have been posted yet.