ReactiveCocoa Essentials: Understanding and Using RACCommand

The source code accompanying this blog post is on github: https://github.com/olegam/RACCommandExample

Is RACCommand your new best friend?

The RACCommand is one of the essential parts of ReactiveCocoa that eventually can save you a lot of time and help make your iOS or OS X apps more robust.

I’ve met several people new to ReactiveCocoa (hereafter abbreviated RAC) who don’t entirely understand how RACCommand works and when it should be used. So I thought it would be useful to write a small introduction to shed some light. The official documentation doesn’t give many examples of how to use RACCommand, but the comments in the header file are great. However, they may be hard to understand if you’re new to RAC.

The RACCommand class is used to represent the execution of some action. Often the execution is triggered by some action in the UI. Like when the user taps a button. RACCommand instances can be configured to handle reasoning about when it can be executed. This can easily be bound to the UI and the command will also make sure it doesn’t start executing when it’s not enabled. A commonly used strategy for when the command can execute is to leave the allowsConcurrentExecution at it default value of NO. This will make sure the command doesn’t start executing again if it’s already executing. The result of command execution is represented as a RACSignal and can hence yield results with next: (representing new values or results), completed or error:. I’ll show how this is used below.

The example app

Let’s assume we are making a simple iOS app that will let the user subscribe to an email list. In the simplest form we will have a text field and a button. When the user has entered his email and taps the button the email address should be posted to some webservice. Easy enough. However there are some edge cases that we should make sure to handle to provide the best experience. What happens if the user taps the button twice? How are errors handled? What if the email is invalid? RACCommand can help us handle those cases. I’ve implemented a small app to demonstrate the concepts discussed in this post.

Screenshot of example app

Get the source code for the example app here: https://github.com/olegam/RACCommandExample

With a very simple view controller the app also demonstrates a way to practice the MVVM pattern in iOS apps. Basically the view controller sets up the view hierarchy and instantiates an instance of the view model.

1
2
3
4
5
- (void)bindWithViewModel {
  RAC(self.viewModel, email) = self.emailTextField.rac_textSignal;
  self.subscribeButton.rac_command = self.viewModel.subscribeCommand;
  RAC(self.statusLabel, text) = RACObserve(self.viewModel, statusMessage);
}

The above method (called from viewDidLoad) creates the bindings between the view and the view model. All the interesting stuff is in the view model. It has the following interface:

1
2
3
4
5
6
7
8
9
10
11
@interface SubscribeViewModel : NSObject

@property(nonatomic, strong) RACCommand *subscribeCommand;

// write to this property
@property(nonatomic, strong) NSString *email;

// read from this property
@property(nonatomic, strong) NSString *statusMessage;

@end

The RACCommand property exposed here is what the rest of this post will be about. The two other string properties were bound to properties of the view as shown above. The full implementation of the view model looks like this:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#import "SubscribeViewModel.h"
#import "AFHTTPRequestOperationManager+RACSupport.h"
#import "NSString+EmailAdditions.h"

static NSString *const kSubscribeURL = @"http://reactivetest.apiary.io/subscribers";

@interface SubscribeViewModel ()
@property(nonatomic, strong) RACSignal *emailValidSignal;
@end

@implementation SubscribeViewModel

- (id)init {
  self = [super init];
  if (self) {
      [self mapSubscribeCommandStateToStatusMessage];
  }
  return self;
}

- (void)mapSubscribeCommandStateToStatusMessage {
  RACSignal *startedMessageSource = [self.subscribeCommand.executionSignals map:^id(RACSignal *subscribeSignal) {
      return NSLocalizedString(@"Sending request...", nil);
  }];

  RACSignal *completedMessageSource = [self.subscribeCommand.executionSignals flattenMap:^RACStream *(RACSignal *subscribeSignal) {
      return [[[subscribeSignal materialize] filter:^BOOL(RACEvent *event) {
          return event.eventType == RACEventTypeCompleted;
      }] map:^id(id value) {
          return NSLocalizedString(@"Thanks", nil);
      }];
  }];

  RACSignal *failedMessageSource = [[self.subscribeCommand.errors subscribeOn:[RACScheduler mainThreadScheduler]] map:^id(NSError *error) {
      return NSLocalizedString(@"Error :(", nil);
  }];

  RAC(self, statusMessage) = [RACSignal merge:@[startedMessageSource, completedMessageSource, failedMessageSource]];
}

- (RACCommand *)subscribeCommand {
  if (!_subscribeCommand) {
      NSString *email = self.email;
      _subscribeCommand = [[RACCommand alloc] initWithEnabled:self.emailValidSignal signalBlock:^RACSignal *(id input) {
          return [SubscribeViewModel postEmail:email];
      }];
  }
  return _subscribeCommand;
}

+ (RACSignal *)postEmail:(NSString *)email {
  AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
  manager.requestSerializer = [AFJSONRequestSerializer new];
  NSDictionary *body = @{@"email": email ?: @""};
  return [[[manager rac_POST:kSubscribeURL parameters:body] logError] replayLazily];
}

- (RACSignal *)emailValidSignal {
  if (!_emailValidSignal) {
      _emailValidSignal = [RACObserve(self, email) map:^id(NSString *email) {
          return @([email isValidEmail]);
      }];
  }
  return _emailValidSignal;
}

@end

This might seem like a big chunk so let’s go through it in smaller parts. The RACCommand that we are most interested in is created like this:

1
2
3
4
5
6
7
8
9
- (RACCommand *)subscribeCommand {
  if (!_subscribeCommand) {
      NSString *email = self.email;
      _subscribeCommand = [[RACCommand alloc] initWithEnabled:self.emailValidSignal signalBlock:^RACSignal *(id input) {
          return [SubscribeViewModel postEmail:email];
      }];
  }
  return _subscribeCommand;
}

The command is initialized with an enabledSignal parameter. This is a signal indicating when the command can execute. In our case it should be allowed to execute when the email address entered by the user is valid. The self.emailValidSignal is a signal that sends a NO or a YES every time the email changes.

The signalBlock parameter is invoked every time the command needs to execute. The block should return a signal representing the execution. Since we leave the default property of allowsConcurrentExecution on NO the command will watch this signal and not allow any new executions before the execution in progress completes.

Because the command is set on the button’s rac_command property defined in the UIButtton+RACCommandSupport category the button will automatically change between the enabled and disabled state based on when the command can execute.

Also the command will automatically execute when the button is tapped by the user. We get all this for free with RACCommand. If you need to execute the command manually you can do this by messaging -[RACCommand execute:]. The argument is an optional input. We don’t use it here, but it is often very useful (the button sends itself as input when it calls -execute:). The -execute: method is also one of the places you can hook in to watch the status of the execution. You could potentially do something like this:

1
2
3
[[self.viewModel.subscribeCommand execute:nil] subscribeCompleted:^{
  NSLog(@"The command executed");
}];

In our example the button executes the command for us (so we don’t call -execute:) and hence we have to listen for another property of the command in order to update the UI when the command executes. There are several opportunities to chose between here. And this can maybe be a little confusing. The executionSignals property of RACCommand is a signal that sends a next: every time the commands start executing. The argument is the signal created by the command. So it’s a signal of signals. We use that in the mapSubscribeCommandStateToStatusMessage method of the view model to get a signal with a string value every time the command is started:

1
2
3
RACSignal *startedMessageSource = [self.subscribeCommand.executionSignals map:^id(RACSignal *subscribeSignal) {
  return NSLocalizedString(@"Sending request...", nil);
}];

To get a similar signal with a string every time the command completes execution we have to do a little more work if we want to be purely functional:

1
2
3
4
5
6
7
RACSignal *completedMessageSource = [self.subscribeCommand.executionSignals flattenMap:^RACStream *(RACSignal *subscribeSignal) {
  return [[[subscribeSignal materialize] filter:^BOOL(RACEvent *event) {
      return event.eventType == RACEventTypeCompleted;
  }] map:^id(id value) {
      return NSLocalizedString(@"Thanks", nil);
  }];
}];

The flattenMap: method used above invokes a block with the subscribeSignal when the command executes. This block returns a new signal and it’s values are passed down into the resulting signal. The materialize operator let’s us get a signal of RACEvents (ie. the next: complete and error: messages are delivered as RACEvent instances as next: values on the resulting signal). We can then filter those events to only get the ones from when the signal completes and in that case map it to a string value. Did I loose you here? I hope not, but you may need to look up the documentation of flattenMap: and materialize to better understand what they do.

We could have implemented this behavior in a different way that is less functional, but maybe easier to understand:

1
2
3
4
5
6
7
@weakify(self);
[self.subscribeCommand.executionSignals subscribeNext:^(RACSignal *subscribeSignal) {
  [subscribeSignal subscribeCompleted:^{
      @strongify(self);
      self.statusMessage = @"Thanks";
  }];
}];

However, I don’t like the above implementation as it involves side effects in the blocks. This also has the disadvantage of referring and retaining self in the block. Thus I have to use the @weakify and @strongify macros (defined in the libextobjc pod) to avoid a retain cycle. So better just avoid side effects altogether when possible as I did with the original implementation.

There is an important details to note about the executionSignals property. The signals sent here do not include error events. For those there is a special errors property. A signal that sends any error during execution of the command as a next:. The errors are not sent as regular error: events as that would terminate the signal. We can easily map the the errors to string messages:

1
2
3
RACSignal *failedMessageSource = [[self.subscribeCommand.errors subscribeOn:[RACScheduler mainThreadScheduler]] map:^id(NSError *error) {
  return NSLocalizedString(@"Error :(", nil);
}];

Now when we have 3 signals with status messages we want to show to the user we can merge them into one signal and bind that to the statusMessage property of the view model (bound to the statusLabel.text property of the view controller).

1
RAC(self, statusMessage) = [RACSignal merge:@[startedMessageSource, completedMessageSource, failedMessageSource]];

So this was an example of how a RACCommand can be used in practice in an iOS app. I think this way of implementing logic has many advantages over the way many people would implement it with a UITextFieldDelegate in the view controller and lots of state stored in ivars or properties.

Other RACCommand details of interest

RACCommand has an executing property that is actually a signal sending YES when execute: is invoked and NO when it terminates. Upon subscription the signal will send it’s current value. If you just need to get the current value and don’t want a signal, you can get it immediately like this:

1
BOOL commandIsExecuting = [[command.executing first] boolValue];

The enabled property is also a signal sending YES and NO. It will send NO when the command was created with an enabledSignal and that signal sends a NO or if the signal is executing and allowsConcurrentExecutions is set to NO.

If you try to message -execute: on a signal that is not enabled it will immediately send an error, but that error will not be sent to the errors signal.

The -execute: method will automatically subscribe to the original signal and multicast it. This basically means that you do not have to subscribe to the returned signal, but if you do so you should not be afraid of side effects happening twice.

Comments