ReactiveCocoa From the Ground Floor, Part 2

In the first part of the tutorial, we covered the core concepts of ReactiveCocoa and took a high level overview of the framework, and the concepts needed to be able to use it effectively. Now with that out of the way, we will dive into some more concrete examples and actually get to some code!

So lets expand on the example from the first tutorial of the text field the lets the user know if it is valid or invalid in real-time. So in a real world use case, you will most likely have more than one text field, and you will also have a button that the user should tap whenever the text fields are filled out that will do something like log them in or register them. So for our example we will do what would be a simple registration form with a login button that will perform a network request when tapped. We will have:

  • A text field for the user’s name.
  • A text field for their email address.
  • A text field for their password.
  • A button that “logs in” the user.

And we want to let the user know in real time if each text field is valid or invalid, so next to each field we will display a red start for invalid, and a green start for valid. We also want the button to only be enabled whenever all of the forms are valid, and we want this to update in real-time as well. So lets get to the code!

First, open Xcode and choose the “Single View” template. Then, using Cocoapods install ReactiveCocoa. Once that is set up, open up your Storyboard and set up the interface as is shown below.

And then hook everything up in the .m file and import ReactiveCocoa and EXTScope. Don’t forget the handy GreenStar and RedStar macros at the top!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define GreenStar [UIImage imageNamed:@"GreenStar"]
#define RedStar [UIImage imageNamed:@"RedStar"]

#import "RACTextFieldViewController.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <EXTScope.h>

@interface RACTextFieldViewController () <UITextFieldDelegate>
@property(weak, nonatomic) IBOutlet UITextField *nameField;
@property(weak, nonatomic) IBOutlet UITextField *emailField;
@property(weak, nonatomic) IBOutlet UITextField *passwordField;
@property(weak, nonatomic) IBOutlet UIImageView *nameIndicator;
@property(weak, nonatomic) IBOutlet UIImageView *emailIndicator;
@property(weak, nonatomic) IBOutlet UIImageView *passwordIndicator;
@property(weak, nonatomic) IBOutlet UIButton *createAccountButton;
@property(strong,nonatomic)UIActivityIndicatorView *activityIndicatorView;
@property (weak, nonatomic) IBOutlet UIImageView *imageView;

@end

EXTScope.h provides some helpful macros for avoiding retain cycles, and we will cover those when we get there. Now for the fun stuff.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@implementation RACTextFieldViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.nameField.delegate = self;
    self.emailField.delegate = self;
    self.passwordField.delegate = self;

    ///We want to constantly monitor the correctness of the text fields 
    ///so that we can update the indicators in real time. These are arbitrary values used for correctness, 
    ///you could use whatever you desired.
    RACSignal *nameSignal = [self.nameField.rac_textSignal map:^id(NSString *value) {
        return @(value.length > 4);
    }];
    RACSignal *emailSignal = [self.emailField.rac_textSignal map:^id(NSString *value) {
        return @(value.length > 4 && [value rangeOfString:@"@"].location != NSNotFound);
    }];
    RACSignal *passwordSignal = [self.passwordField.rac_textSignal map:^id(NSString *value) {
        return @(value.length > 6 && ![value isEqualToString:@"password"]);
    }];
    /// More to come...
    }

So after assigning the delegates, we use the rac_textSignal property that ReactiveCocoa provides on UITextField that gives us a signal that sends a next event each time each time the text in the text field is modified, so we will know exactly what the user enters as they enter it. Then we use the map: function and return a BOOL that indicates if the field is valid or not. Here we use arbitrary length’s (and check for the “@” character in the email field) to determine validity, but you could use whatever you want.

The map: function returns a signal, so we get a signal from each mapping, and we will use those signals to set the image views to the correct image. Here we use the GreenStar and RedStar macros declared earlier to set the image.

1
2
3
4
5
6
7
8
9
10
11
12
13
///We want to indicate to the user exactly when each field goes from incorrect to correct, or vice-versa;
    RAC(self.nameIndicator.image) = [nameSignal map:^id(NSNumber *value) {
        if(value.boolValue) return GreenStar;
        else return RedStar;
    }];
    RAC(self.emailIndicator.image) = [emailSignal map:^id(NSNumber *value) {
        if(value.boolValue) return GreenStar;
        else return RedStar;
    }];
    RAC(self.passwordIndicator.image) = [passwordSignal map:^id(NSNumber *value) {
        if(value.boolValue) return GreenStar;
        else return RedStar;
    }];

Each time the user enters a something on one of the text fields, the respective signal received from the previous mapping will be sent the BOOL value as a next event. So here, what we do is use the handy map: function again, and if the value is true (indicating the field is valid) we return a green star, or if it is false we return a red star. Then we use the RAC macro to bind each image views image property to the value of the signal that is returned from the map: function. So every time nameSignal, emailSignal, or passwordSignal receive a next event, which is each time the user enters or removes a character from the text field, the respective image view will be updated.

So right here, with this very little bit of code we have text fields that will indicate their validity to the user in real-time. Now lets move on to the button. We want the button to only be enabled when all of the text fields are valid, and again, ReactiveCocoa makes this incredibly easy.

1
2
3
4
5
///Only enable the create account button when each field is filled out correctly.
    RACSignal *correctnessSignal = [RACSignal combineLatest:@[nameSignal, emailSignal, passwordSignal]
     reduce:^(NSNumber *name, NSNumber *email, NSNumber *password) {
        return @((name.boolValue && email.boolValue && password.boolValue));
    }];

There is a super handy class method on RACSignal called combineLatest: reduce:, and what combineLatest: reduce: does is take an array of signals and combine them into a single signal that will send the lastest value from each of the signals in the array passed as a parameter. then the reduce: method takes a block that will be sent the lastest values from the combined signals, which here would be an NSNumber wrapped BOOL. So we simply reduce all of those values to a single BOOL value that indicates when all of the text fields are valid, and the we return that and get a new signal that indicates the validity of the text fields.

Now we can move into setting up the button:

1
2
3
///A RACCommand is used for buttons in place of adding target actions. In this case, 
///we only want the command to be able to execute if the correctnessSignal returns true.
    RACCommand *command = [RACCommand commandWithCanExecuteSignal:correctnessSignal];

For the button, instead of setting a target/action pair in order to call a method, we will use a RACCommand. A command is simply a signal that is triggered in response to some event, such as a button press, and is generally used to replace target action pairs on UIButtons or UIBarButtonItems. So we instantiate the command with the commanfWithCanExecuteSignal: method, and what this does it take a signal that returns a BOOL, and it sets the commands canExecute property with the value received from that signal. This will come in handy in a bit.

Now we need to set up the action that will be performed whenever the user taps the button. So we will take the command and use the addSignalBlock: method.

1
2
3
4
5
6
7
8
9
///Here we return a signal block that execute a network request on a background thread. 
///If the success parameter is set = NO, then the error will be sent.
    RACSignal *comnandSignal = [command addSignalBlock:^RACSignal *(id value) {
        return [RACSignal start:^id(BOOL *success, NSError *__autoreleasing *error) {
           NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://farm9.staticflickr.com/8109/8631724728_48c13f7733_b.jpg"]];
            *success = (data != nil);
            return [UIImage imageWithData:data];
        }];
    }];

The addSignalBlock: method requires that a RACSignal be returned from it, so we use the RACSignal class method start:, which returns a signal, and we return that. Then we do our work inside the block that is passed as a parameter to the start: method. All we want to do when the button is tapped is make a network request and fetch a picture, so we simply grab a NSData object from a URL and turn that into a UIImage and return it. Now before we go on, take notice of the line of code: *success = (data != nil);. What he have here is a pointer to a BOOL, and if that BOOL gets set to NO then the signal will send an NSError rather than the UIImage that we want.

Now we need to make sure we handle the error, and the way we do that is like so:

1
2
3
4
5
6
7
8
9
///Here we catch the error and suppress it, otherwise the signal would complete 
///and the textViews text would never be able to receive a value.
    RACSignal *commandSignalMapped = [comnandSignal map:^id(RACSignal *request) {
        return [request catch:^RACSignal *(NSError *error) {
            ///handle the error;
            NSLog(@"The error is %@", error);
            return [RACSignal empty];
        }];
    }];

Here we map: the commandSignal, which will be giving us RACSignal, then we use catch on that signal, which will do just like it says and catch any errors that the signal sends, then we simply log the error and return [RACSignal empty], which is a signal of nothingness. So what this does is prevent an error from making it any farther than this and possibly getting sent to something that is not expecting it. That is why we use [RACSignal empty], so if there is an error, anything further down the line that is expecting a value from this signal won’t receive an NSError object when it is expecting something else, which could cause a crash.

Now lets hook this command up to our button.

1
2
3
///We set the command to be executed whenever the button is pressed.
    RACSignal *buttonSignal = [self.createAccountButton rac_signalForControlEvents:UIControlEventTouchUpInside];
    [buttonSignal executeCommand:command];

ReactiveCocoa gives UIbutton a method called rac_signalForControlEvents: that will produce a signal that sends a next event each time the control event is recognized. We want the button to execute the command we set up, so we simply pass the command to the executeCommand: method of the buttonSignal. Now each time that buttonSignal sends a next event, it will execute the code in the commands addSignalBlock: method.

1
2
3
4
5
6
7
8
@weakify(self);
    ///Hide the keyboard whenever the button is pressed. This would be considered a side effect.
    [buttonSignal subscribeNext:^(id x) {
        @strongify(self)
        [self.nameField resignFirstResponder];
        [self.emailField resignFirstResponder];
        [self.passwordField resignFirstResponder];
    }];

Now we get to use the macros provided by EXTScope. @weakify(self) and @strongify(self) is just syntactic sugar for preventing retain cycles on self. So we simply use @weakify(self) once before a block where we need to reference self, and then use @strongify(self) any other time we reference self inside a block to prevent self from getting de-allocated early. The rest is just clean up, as we want to make sure all the keyboards are dismissed whenever the button is pressed. So we simply dismiss the keyboard whenever buttonSignal sends a next event.

Now the button is set up, and it will fire the command, whenever it is tapped, and the command will only execute when all the text fields are valid, but we need to hook up the button to be enabled or disabled depending on the validity of the fields, and to do that all we have to do is this:

1
2
3
4
5
///We don't want the button to be pressed while the command is executing, 
///so we set its enabledness based on the command's canExecute property. 
///Note that we deliver it on the main thread, since we are binding to a UIKit property.
    RAC(self.createAccountButton.enabled) = [RACAbleWithStart(command, canExecute) 
                                              deliverOn:[RACScheduler mainThreadScheduler]];

We use the RACAbleWithStart macro to create a signal based on the canExecute property of the command, then we use deliverOn: in order to make sure the signal is sent on the main thread, since we are binding to a UIKit property, and doing anything with UIKit off of the main thread is a no-no. Using RACAbleWithStart with send a signal with the starting value of whatever is passed into it, so here we want to start with the value of the commands canExecute property, which will be NO whenever the view is first loaded, since all the text fields start out invalid.

So now the button is hooked up and will fire off its command, so lets do something with the results of that command. Since we get an image, lets display it to the user.

1
2
3
4
5
///Here we bind the imageView's image property to the signal sent from the command. 
///We flatten it because the commandSignalMapped is a signal of signals, and flattening is the same as merging, 
///so we get one signal that represents the value of all of the signals. Note again the delivery on the main thread.
    RAC(self.imageView.image) = [[commandSignalMapped flatten]
                                  deliverOn:[RACScheduler mainThreadScheduler]];

Here we take commandSignalMapped and apply the flatten method to it. We need to do this because commandSignalMapped is a signal of signals, and flatten takes a signal of signals and flattens, or merges, it into a new signal that represents the value of all of the signals. So we bind the image property of the imageView to the signal from the flattening of the commandSignalMapped, which will send the image that we fetch when the button is tapped. Note again the use of deliverOn: since we are binding to UIKit.

Now everything is hooked up and will function just like we want it to and we have not crested 100 lines of code! Now lets do one final thing, lets set up an activity indicator to indicate to the user that we are processing the “registration”.

1
2
3
4
5
6
7
///The activityIndicator will be spin while the command is being executed.
    self.activityIndicatorView = [[UIActivityIndicatorView alloc]init];
    UIBarButtonItem *item = [[UIBarButtonItem alloc]initWithCustomView:self.activityIndicatorView];
    UIBarButtonItem *space = [[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
    space.width = 20; //make the activity spinner offset from the edge of the nav bar
    self.activityIndicatorView.color = [UIColor blackColor];
    self.navigationItem.rightBarButtonItems = @[space, item];

Here we just set up an activity indicator as we normally would. Then what we want to do is have the indicator be active while the command the button triggers is executing, and we want it to stop when the command stops. So we get a new signal to help us.

1
2
3
4
5
6
7
8
9
///Since we cannot set the activityIndicators animating property directly, 
///we have to invoke its methods as side effects.
    RACSignal *commandSignal = [RACAble(command, executing) 
                                  deliverOn:[RACScheduler mainThreadScheduler]];
    [commandSignal subscribeNext:^(NSNumber *x) {
        @strongify(self)
        if(x.boolValue) [self.activityIndicatorView startAnimating];
        else [self.activityIndicatorView stopAnimating];
    }];

We make use of the command again, but this time we want to watch its ‘executing’ property. So we use the RACable macro, which is just like RACAbleWithStart except it does not send the starting value, it waits for a value to be sent to it first before sending a next event. Then we use subscribeNext: on the new signal and start or stopp the activity indicator depending on the value it sends, which will be YES when the command starts executing, and NO when it stops. Notice again the use of the @strongify macro, since we are referring to self inside the block.

And thats it! Now the app reacts in real-time to all of the users input, and gives the user feedback for every action. And all this with barely over 100 lines of code! All the code show here is available on GitHub. It is part of a larger project that shows different uses of ReactiveCocoa. You can get see all the code from this tutorial by looking at the “Text Fields” example.

Not only does ReactiveCocoa provide you with incredibly easy methods for creating, well, reactive apps, but it also provides the ability to write declarative code that more clearly communicates your intent and generates less errors. Personally, I think that ReactiveCocoa is the future of Cocoa programming, and with ReactiveCocoa 2.0 right around the corner, now is as good a time as any to jump on the train.