
If you made it through installing ReactiveCocoa, via Cocoapods, which required you to have ruby, update it and install Cocoapods and CLT, then you’re already ahead! So here is RAC. RAC is used when you need to be notified of something important in your app; completed download, some long asynchronous task like data fetching, parsing or image processing is complete or to update UI such as a completed form produces an active Submit button or an incomplete field yields a red warning sign to the user.
How does RAC work? You basically create signals for events you are interested in (like the ones mentioned above) and then you tie those signals to a control to be notified. The signal will have the format:
RACSignal (property) someFiringEvent:^doThisBlock(){
This block will execute after signal is fired;
}];
They can also get more complex like:
RACSignal (property) someFiringEvent:^doThisBlock(){
This block will execute after signal is fired;
}]
someOtherFiringEvent:^doThisOtherBlock(){
This other block will execute after the previous firing event;
}];
It seems 99% of the articles out there give you hundreds of partial snippets of how to do something. I’ll give you a nice, concise and complete project.
(for tips on installing all these things:http://www.raywenderlich.com/12139/introduction-to-cocoapods && http://cocoapods.org && http://www.raywenderlich.com/55384/ios-7-best-practices-part-1). These all use Cocoapods installation for their projects but Ill be posting a complete article on how to do this next!
As a pre-requisite, create a SingleView project in Xcode (5.0.2 at the time of writing) and you get a ViewController class with storyboard. Add a class called LoginModel as a subclass of NSObject to your project. These are all the classes you are going to need. Now let’s add RAC; so close Xcode and install the Reactive Cocoa pod which will create the workspace. Now open Xcode back up and open up your workspace, not your xcodeproj file.
So here it is…
You need to conceptually:
1. Create a signal method in the Model Class.
2. Connect the view controller properties to the Model properties
3. Call the signal method from the view controller.
Sounds simple enough!
Before we get started, don’t forget to add the ReactiveCocoa import to the prefix.pch file in your project.
First, add this to the LoginModel.h
@property(nonatomic, strong) NSString *username;
@property(nonatomic, strong) NSArray *forbiddenNames;
-(RACSignal *)forbiddenNameSignal;
and add this to your LoginModel.m
-(id)init {
self = [super init];
if(!self) return nil;
self.forbiddenNames = @[ @"Peter",@"Piper",@"Picker"];
return self;
}
-(RACSignal *)forbiddenNameSignal {
return [RACObserve(self, username) filter:^BOOL(NSString *newName) {
return [self.forbiddenNames containsObject:newName];
}];
}
We are simply declaring a RACSignal method to be called. What it does is check (filter) any names passed to it to make sure it doesn’t match anything in the forbiddenNames array. The cool thing is that it doesn’t just wait to be passed that info in order to check it…it observes for any changes in the value of the LoginModel property self.username.
Now let’s move over to the view controller, the UI!
Second, drop a UITextField onto your view controller scene in storyboard and connect it to an IBOutlet UITextField *usernameField; by adding this to your ViewController.h:
@property (nonatomic,strong) IBOutlet UITextField *usernameField;
and this to your ViewController.m:
#import "LoginModel.h"
@interface ViewController ()
@property (nonatomic,strong) NSString *username;
@property (nonatomic,strong) LoginModel *mymodel;
@end
@implementation ViewController
- (void)viewDidLoad{
[super viewDidLoad];
self.mymodel = [LoginModel new]; //1
RAC(self.mymodel, username) = self.usernameField.rac_textSignal; //2
[self.usernameField.rac_textSignal subscribeNext:^(NSString *x) {
self.username = x;
}]; //3
[self.mymodel.forbiddenNameSignal subscribeNext:^(NSString *name) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Forbidden Name!"
message:[NSString stringWithFormat:@"The name %@ has been forbidden!",name]
delegate:nil cancelButtonTitle:@"Ok" otherButtonTitles:nil];
[alert show];
self.mymodel.username = @"";
}]; //4
- Here we instantiate a LoginModel object,
- Then make a connection from the vc property to the model property
- Then we create a signal to make sure we update the model username when the vc username changes and finally we call the model method to check (filter) the values.
That’s it! Run the app and you should get your textfield displayed. If you type in any of the names in the forbiddenNames array, you get a UIAlertView.
This of course is the simplest thing you can do with RAC. Actually, there is a simpler thing you can do:
- Just Observe, without reacting
[RACObserve(self, username) subscribeNext:^(NSString *newName) {
NSLog(@"%@", newName);
}];
- Observe && React, by filtering (like we just did)
[[RACObserve(self, username)
filter:^(NSString *newName) {
return [newName hasPrefix:@"j"];
}]
subscribeNext:^(NSString *newName) {
NSLog(@"%@", newName);
}];
or something a little more useful like check for a specific name:
[[[[RACAbleSelf(self.username) distinctUntilChanged] take:3]
where:^(NSString *newUsername) {
return [newUsername isEqualToString:@"marciokoko"];
}]
subscribeNext:^(id _) {
NSLog(@"Welcome back!");
}];
- Observe 2 objects and combine the observation into 1 signal & reduce those 2 objects into 1 return value (here you have to view the changing value of BOOL createEnabled in the debugger with breakpoints*)
RAC(self, createEnabled) = [RACSignal
combineLatest:@[ RACObserve(self, password), RACObserve(self, passwordConfirmation) ]
reduce:^(NSString *password, NSString *passwordConfirm) {
return @([passwordConfirm isEqualToString:password]);
}];
- Add a notification or signal to a merge process that doesn’t have one (what about the result?)
[[RACSignal merge:@[ self.words1, self.words2 ]]
subscribeCompleted:^{
NSLog(@"They're both done!");
[self.tableView reloadData];*
}];
- Map stuff :s
RACSignal *letters = [@"A B C DE F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;
[letters subscribeNext:^(NSString *x) {
NSLog(@"%@", x);
}];
or something like:
RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;
RACSequence *mapped = [letters map:^(NSString *value) {
return [value stringByAppendingString:value];
}];
- This is one I like for processing arrays 🙂 *
RACSequence *normalizedLongWords = [[words.rac_sequence
filter:^BOOL (NSString *word) {
return [word length] >= 10;
}]
map:^(NSString *word) {
return [word lowercaseString];
}];
- LoginCommand which we will see in a future post!
All this might seem trivial, but when you get into complex apps where you need to fetch stuff from the web, authenticate, run complex CoreData queries etc, Blocks, Completion Handlers and RAC are your best friends. I always like to think of the alternative. What would you have to do, without RAC, to monitor a change in property or value:
[self addObserver:forKeyPath:options:context];
[self observerValueForKeyPath:change:context:]
if (context ==& someContext)
if ([keyPath isEqualToString @"someProperty"])
//do whatever you need in here
[self removeObserver:forKeyPath:context];
All that! Or worst, delegates or NSNotifications…
RAC is actually based on KVO anyway but with the added advantage that it has a nice wrapper API around it 🙂
Lastly * results! This might be the most important part of this article. You noticed I have a few * thrown around. I guess the thing that most threw me off about RAC was that in some of the simplest cases, like observing a name property change, an NSLog would confirm that everything had worked out. However in many other cases, specifically the ones with *, there is no result!
I just want to thank Colin Eberhardt & Colin Wheeler as well as Ray Wenderlich for helping me out with this stuff!