ReactiveCocoa Tutorial – The Definitive Introduction: Part 1/2
Get to grips with ReactiveCocoa in this 2-part tutorial series. Put the paradigms to one-side, and understand the practical value with work-through examples. By Colin Eberhardt.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
ReactiveCocoa Tutorial – The Definitive Introduction: Part 1/2
35 mins
Reactive Sign-in
The application currently uses the reactive pipelines illustrated above to manage the state of the text fields and button. However, the button press handling still uses actions, so the next step is to replace the remaining application logic in order to make it all reactive!
The Touch Up Inside event on the Sign In button is wired up to the signInButtonTouched
method in RWViewController.m
via a storyboard action. You’re going to replace this with the reactive equivalent, so you first need to disconnect the current storyboard action.
Open up Main.storyboard, locate the Sign In button, ctrl-click to bring up the outlet / action connections and click the x to remove the connection. If you feel a little lost, the diagram below kindly shows where to find the delete button:
You’ve already seen how the ReactiveCocoa framework adds properties and methods to the standard UIKit controls. So far you’ve used rac_textSignal
, which emits events when the text changes. In order to handle events you need to use another of the methods that ReactiveCocoa adds to UIKit, rac_signalForControlEvents
.
Returning to RWViewController.m
, add the following to the end of viewDidLoad
:
[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext:^(id x) {
NSLog(@"button clicked");
}];
The above code creates a signal from the button’s UIControlEventTouchUpInside
event and adds a subscription to make a log entry every time this event occurs.
Build and run to confirm the message actually logs. Bear in mind that the button will enable only when the username and password are valid, so be sure to type some text into both fields before tapping the button!
You should see messages in the Xcode console similar to the following:
2013-12-28 08:05:10.816 RWReactivePlayground[18203:a0b] button clicked
2013-12-28 08:05:11.675 RWReactivePlayground[18203:a0b] button clicked
2013-12-28 08:05:12.605 RWReactivePlayground[18203:a0b] button clicked
2013-12-28 08:05:12.766 RWReactivePlayground[18203:a0b] button clicked
2013-12-28 08:05:12.917 RWReactivePlayground[18203:a0b] button clicked
Now that the button has a signal for the touch event, the next step is to wire this up with the sign-in process itself. This presents something of a problem — but that’s good, you don’t mind a problem, right? Open up RWDummySignInService.h
and take a look at the interface:
typedef void (^RWSignInResponse)(BOOL);
@interface RWDummySignInService : NSObject
- (void)signInWithUsername:(NSString *)username
password:(NSString *)password
complete:(RWSignInResponse)completeBlock;
@end
This service takes a username, a password and a completion block as parameters. The given block is run when the sign-in is successful or when it fails. You could use this interface directly within the subscribeNext:
block that currently logs the button touch event, but why would you? This is the kind of asynchronous, event-based behavior that ReactiveCocoa eats for breakfast!
Creating Signals
Fortunately, it’s rather easy to adapt existing asynchronous APIs to be expressed as a signal. First, remove the current signInButtonTouched:
method from the RWViewController.m. You don’t need this logic as it will be replaced with a reactive equivalent.
Stay in RWViewController.m and add the following method:
-(RACSignal *)signInSignal {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[self.signInService
signInWithUsername:self.usernameTextField.text
password:self.passwordTextField.text
complete:^(BOOL success) {
[subscriber sendNext:@(success)];
[subscriber sendCompleted];
}];
return nil;
}];
}
The above method creates a signal that signs in with the current username and password. Now for a breakdown of its component parts.
The above code uses the createSignal:
method on RACSignal
for signal creation. The block that describes this signal is a single argument, and is passed to this method. When this signal has a subscriber, the code within this block executes.
The block is passed a single subscriber
instance that adopts the RACSubscriber
protocol, which has methods you invoke in order to emit events; you may also send any number of next events, terminated with either an error or complete event. In this case, it sends a single next event to indicate whether the sign-in was a success, followed by a complete event.
The return type for this block is an RACDisposable
object, and it allows you to perform any clean-up work that might be required when a subscription is cancelled or trashed. This signal does not have any clean-up requirements, hence nil
is returned.
As you can see, it’s surprisingly simple to wrap an asynchronous API in a signal!
Now to make use of this new signal. Update the code you added to the end of viewDidLoad
in the previous section as follows:
[[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
map:^id(id x) {
return [self signInSignal];
}]
subscribeNext:^(id x) {
NSLog(@"Sign in result: %@", x);
}];
The above code uses the map
method used earlier to transform the button touch signal into the sign-in signal. The subscriber simply logs the result.
If you build and run, then tap the Sign In button, and take a look at the Xcode console, you’ll see the result of the above code …
… and the result isn’t quite what you might have expected!
2014-01-08 21:00:25.919 RWReactivePlayground[33818:a0b] Sign in result:
<RACDynamicSignal: 0xa068a00> name: +createSignal:
The subscribeNext:
block has been passed a signal all right, but not the result of the sign-in signal!
Time to illustrate this pipeline so you can see what’s going on:
The rac_signalForControlEvents
emits a next event (with the source UIButton
as its event data) when you tap the button. The map step creates and returns the sign-in signal, which means the following pipeline steps now receive a RACSignal
. That is what you’re observing at the subscribeNext:
step.
The situation above is sometimes called the signal of signals; in other words an outer signal that contains an inner signal. If you really wanted to, you could subscribe to the inner signal within the outer signal’s subscribeNext:
block. However it would result in a nested mess! Fortunately, it’s a common problem, and ReactiveCocoa is ready for this scenario.
Signal of Signals
The solution to this problem is straightforward, just change the map
step to a flattenMap
step as shown below:
[[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
flattenMap:^id(id x) {
return [self signInSignal];
}]
subscribeNext:^(id x) {
NSLog(@"Sign in result: %@", x);
}];
This maps the button touch event to a sign-in signal as before, but also flattens it by sending the events from the inner signal to the outer signal.
Build and run, and keep an eye on the console. It should now log whether the sign-in was successful or not:
2013-12-28 18:20:08.156 RWReactivePlayground[22993:a0b] Sign in result: 0
2013-12-28 18:25:50.927 RWReactivePlayground[22993:a0b] Sign in result: 1
Exciting stuff!
Now that the pipeline is doing what you want, the final step is to add the logic to the subscribeNext
step to perform the required navigation upon successful sign-in. Replace the pipeline with the following:
[[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
flattenMap:^id(id x) {
return [self signInSignal];
}]
subscribeNext:^(NSNumber *signedIn) {
BOOL success = [signedIn boolValue];
self.signInFailureText.hidden = success;
if (success) {
[self performSegueWithIdentifier:@"signInSuccess" sender:self];
}
}];
The subscribeNext:
block takes the result from the sign-in signal, updates the visibility of the signInFailureText
text field accordingly, and performs the navigation segue if required.
Build and run to enjoy the kitten once more! Meow!
Did you notice there is one small user experience issue with the current application? When the sign-in service validates the supplied credentials, is should disable the Sign In button. This prevents the user from repeating the same sign-in. Furthermore, if a failed sign-in attempt occurred, the error message should be hidden when the user tries to sign-in once again.
But how should you add this logic to the current pipeline? Changing the button’s enabled state isn’t a transformation, filter or any of the other concepts you’ve encountered so far. Instead, it’s what is known as a side-effect; or logic you want to execute within a pipeline when a next event occurs, but it does not actually change the nature of the event itself.