Update: This article has been updated to reflect the changes in the latest stable ReactiveCocoa v2
MVC - One Pattern to Rule them all
The MVC design pattern has existed since the late 1970s and has been in use in the Foundation, AppKit & UIKit Frameworks for a long time now. At its heart is a very simple design and a good idea. The Design is this
As you can see we have 3 components
- Model - The Data model our view is managing
- View - The User Interface on screen consisting of things such as text fields, buttons, etc.
- Controller - The Controller in MVC exists to decouple the view from the model.
In the MVC design pattern, the controller separates the model from the view so that each can change independently. If we didn't have such a decoupling then every time the model changed, the view would need to change as well. In this design pattern interactions happen like this
- A view triggers an action to happen on the Controller
- The Controller executes said action and updates the model
- The controller may receive a notification from the model that it was updated.
- The Controller updates the view
This is the MVC waltz that we do when working with this design pattern, overall it works fairly well. The reason we use this is because this pattern decouples the view from the data model, letting the controller be an intermediary between the 2. This allows the controller to access the parts of the data model that need to be accessed and allows for the controller to do things like properly format the data for the view.
MVVM - For a Better World
MVVM stands for Model-View-ViewModel, and comes from Microsoft and is based off of the MVC design pattern. To be more precise, it is based off of Martin Fowlers Presentation Model. To some people at first it looks very similar to the MVC design pattern and people become confused as to how its different. So let me show you the pattern and then we'll see how its different
In this pattern we still have 3 components, the same as MVC however there are differences. In MVVM I and others tend to group the View Controller in with the View so that those 2 things are treated as 1 component. The most important part is that we've introduced the View Model and that the view gets updates pushed at it from the view model through some sort of observer. In this article ReactiveCocoa will be that observer.
In the MVVM pattern the View Model encapsulates data/properties that the view can bind to and any validation logic and actions that can be performed. For Instance if you had a button that needs to change its title text you would have a property on the view model that the button can bind its title property to. The same goes if you need to change the color of a control or enable and disable the control. In this pattern we are essentially taking the state of our app and putting it into a view model. Its also good to note that as far as the View Model is concerned it doesn't care where it gets this state from. It doesn't matter if it gets it from its init method, a file on disk, Core Data, a database, etc.
Because we are encapsulating the state of our view in such a way this has several benefits.
We don't need to test the UI
In terms of standard controls we don't need to test our UI's at all since the controls are just binding their state to the view model. Apple should be testing the controls that we are using, and you should be testing the custom controls you create. This means that as we bind our controls state to the view model we don't need to query the controls for their state as we should be able to access it within the view model itself. This feature tends to lending itself well to the next feature...
We can easily Unit Test the View Model
The View model has access to all the properties the views are binding to, and because of this we can have validation logic on the view model. This also makes it extremely easy to unit test the model and unit test the various states our UI can get into. I think Justin actually phrased this excellently "A good litmus test for MVVM is whether you’re able to write automated tests for your UI behavior without actually having a live UI." I completely agree with this and think it excellently encapsulates the core idea of MVVM.
Its easier to change the UI
Because the UI is just binding to the view model, this means you should be able to swap components out and just bind the apppropriate properties to the view model and your UI should work the same as before. This should also make it easier for UI designers to experiment around with different views in your app.
ReactiveCocoa: An introduction
As I mentioned ReactiveCocoa is an implementation of Reactive Extensions .NET for Cocoa. The API is not an exact mapping of the API from Rx to Cocoa as explained in the project docs. ReactiveCocoa is also built using the concept of Functional Reactive Programming.
Functional Reactive Programming (FRP) is a programming paradigm for writing
software that reacts to change.
FRP is built on the abstraction of values over time. Rather than capturing
a value at a particular time, FRP provides signals that capture the past,
present, and future value. These signals can be reasoned about, chained,
composed, and reacted to.
By combining signals, software can be written declaratively, without the need
for code that continually observes and updates values. A text field can be
directly set to always show the current timestamp, for example, instead of using
additional code that watches the clock and updates the text field every second.
Signals can also represent asynchronous operations, much like futures and
promises. This greatly simplifies asynchronous software, including networking
code.
One of the major advantages of FRP is that it provides a single, unified
approach to dealing with different types of reactive, asynchronous behaviors.
Lets jump right into ReactiveCocoa through a couple examples and explain what is going on.
[RACObserve(self,name) subscribeNext:^(NSString *newName){
NSLog(@"Name changed to %@",newName);
}];
self.name = @"Hypnotoad";
self.name = @"Nibbler";
When this runs it'll give the output "Name changed to Hypnotoad" and then "Name changed to Nibbler". What we did was create a signal around a key path in self, and then subscribed to that signal and received events on the stream from that signal. Think of it this way, once we create a signal around a key path in this manner anytime the value of that key path changes the signal will see this and send a -next signal with the updated value. We automatically subscribed to this signal when we called -subscribeNext on it.
Signals only send 3 types of messages
- next : This is how we get updated values from the stream
- error : This is sent anytime the signal could not complete
- completed : Sent to let you know that there will be no more signals on the stream
Lets create a signal of our own so you can see what is going on here
-(RACSignal *)urlResults {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSError *error;
NSString *result = [NSString stringWithContentsOfURL:[NSURL URLWithString:@"http://www.google.com"]
encoding:NSUTF8StringEncoding
error:&error];
if (!result) {
[subscriber sendError:error];
} else {
[subscriber sendNext:result];
[subscriber sendCompleted];
}
return nil;
}];
}
Although we could achieve this through other means, I wanted to create this manually. This method returns a signal which was created to get the string contents of a URL address and then return them to us. For this I used NSStrings built in method to retrieve a string from a URL. All we need to do is check to see if we have a result and if we don't then we can send -next and then -completed because we don't intend to send anymore messages.
Besides receiving updated values which we'll likely use for assignment to variables, signals also can be used in validation.
NSCharacterSet *charSet = [NSCharacterSet characterSetWithCharactersInString:@" "];
RACSignal *nameFieldValid = [RACSignal combineLatest:@[ self.usernameField.rac_textSignal, self.passwordField.rac_textSignal ]
reduce:^(NSString *username, NSString *password) {
return @((username.length > 0) && (password.length > 0) &&
([username rangeOfCharacterFromSet:charSet].location == NSNotFound));
}];
This particular example would work on iOS if you had a Username field, password field and you wanted to know if it was filled out correctly. For the purposes of this example we will only concern ourselves with making sure the username & password has a length greater than 0 and doesn't have any spaces in the user name. The combineLatest part of the method makes sure that we get the latest values from the textfield signal, then in the reduce section we are simply returning a number (0 or 1) to indicate if this is valid. When we hook this up to the enabled property of a control we get...
RAC(self.loginButton.enabled) = nameFieldValid;
and from there on out we get a button that is automatically enabled & disabled when the correct conditions arise from the signal we binded it to. Similarly we can bind controls to a view model like so...
RAC(self.textField.text) = RACObserve(self.viewModel,title);
What is going on here is that ReactiveCocoa is implicitly setting up all the necessary infastructure to bind a field to a signal. If you expand the RAC fields you get something like...
[RACSubscriptingAssignmentTrampoline trampoline][
[[RACSubscriptingAssignmentObjectKeyPathPair alloc] initWithObject:self
keyPath:@(((void)(__objc_no && ((void)self.self.resultLabel.string, __objc_no)),
"self.resultLabel.string"))] ] =
[self rac_signalForKeyPath:@(((void)(__objc_no &&
((void)self.self.aTitle, __objc_no)), "self.aTitle"))
observer:self];
Be thankfull you don't have to write all that every time you wanted to bind properties in your code.
What is the ViewModel?
So i mentioned the View Model earlier as being the thing that our user interface binds its properties to as well as containing the actions that can be done to the view model and any validation logic. Lets show a simple example and get started figuring out how the VM in MVVM works. Here is a screenshot of the UI we'll work with...
And here is the ViewModel interface...
#import <Foundation/Foundation.h>
@interface CDWPlayerViewModel : NSObject
@property(nonatomic, retain) NSString *playerName;
@property(nonatomic, assign) double points;
@property(nonatomic, assign) double stepAmount;
@property(nonatomic, assign) double maxPoints;
@property(nonatomic, assign) double minPoints;
@property(nonatomic, readonly) NSUInteger maxPointUpdates;
-(IBAction)resetToDefaults:(id)sender;
-(IBAction)uploadData:(id)sender;
-(RACSignal *)forbiddenNameSignal;
-(RACSignal *)modelIsValidSignal;
@end
and the implementation
#import "CDWPlayerViewModel.h"
@interface CDWPlayerViewModel ()
@property(nonatomic, retain) NSArray *forbiddenNames;
@property(nonatomic, readwrite) NSUInteger maxPointUpdates;
@end
@implementation CDWPlayerViewModel
-(id)init {
self = [super init];
if(!self) return nil;
_playerName = @"Colin";
_points = 100.0;
_stepAmount = 1.0;
_maxPoints = 10000.0;
_minPoints = 0.0;
_maxPointUpdates = 10;
//I guess we'll go with the ned flanders bad words
//change this to whatever you want
_forbiddenNames = @[ @"dag nabbit",
@"darn",
@"poop"
];
return self;
}
-(IBAction)resetToDefaults:(id)sender {
self.playerName = @"Colin";
self.points = 100.0;
self.stepAmount = 1.0;
self.maxPoints = 10000.0;
self.minPoints = 0.0;
self.maxPointUpdates = 10;
self.forbiddenNames = @[ @"dag nabbit",
@"darn",
@"poop"
];
}
-(IBAction)uploadData:(id)sender {
@weakify(self);
[[RACScheduler scheduler] schedule:^{
sleep(1);
//pretend we are uploading to a server on a backround thread...
//dont ever put sleep in your code
//upload player & points...
[[RACScheduler mainThreadScheduler] schedule:^{
@strongify(self);
NSString *msg = [NSString stringWithFormat:@"Updated %@ with %.0f points",self.playerName,self.points];
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Upload Successfull" message:msg delegate:nil
cancelButtonTitle:@"ok" otherButtonTitles:nil];
[alert show];
}];
}];
}
-(RACSignal *)forbiddenNameSignal {
@weakify(self);
return [RACObserve(self,playerName) filter:^BOOL(NSString *newName) {
@strongify(self);
return [self.forbiddenNames containsObject:newName];
}];
}
-(RACSignal *)modelIsValidSignal {
@weakify(self);
return [RACSignal
combineLatest:@[ RACObserve(self,playerName), RACObserve(self,points) ]
reduce:^id(NSString *name, NSNumber *playerPoints){
@strongify(self);
return @((name.length > 0) &&
(![self.forbiddenNames containsObject:name]) &&
(playerPoints.doubleValue >= self.minPoints));
}];
}
@end
A short note about @weakify(self);
and @strongify(self);
, this is one of the things you can do in addition to using __weak id bself = self;
in your apps to avoid retaining self. This particular bit of code is was originally from libextobjc and then incorporated directly into ReactiveCocoa itself. In particular this has the same effect of creating a weak reference to self. I like this approach because you just keep using self.[property]...
in your code and you don't have to constantly remember to create a bself variable. Check out EXTScope.h
in libextobjc or RACEXTScope.h
in ReactiveCocoa.
as you can see its relatively simple because we are only really concerned with a couple properties related to the player. The other properties are for controls to bind to and for some validation. We also have a single action method and a couple signals. The one thing you'll notice is the View Model has no clue what the UI it'll bind to is like at all. This is great because it means you have flexibility in what you bind things to and you can hook up to the view model in ways that make sense for the class thats binding to it.
Now lets look in our ViewController and how it is using these properties and signals...
__weak CDWViewController *bself = self;
To make sure we don't retain self in our blocks we need to use a weak reference. The ReactiveCocoa Project also has @weakify and @strongify for doing this in blocks, but i'll let you investigate that and see if you want to use it.
//Create the View Model
self.viewModel = [CDWPlayerViewModel new];
obviously start with creating a ViewModel instance
//Start Binding our properties
RAC(self.nameField.text) = [RACObserve(self.viewModel,playerName) distinctUntilChanged];
[[self.nameField.rac_textSignal distinctUntilChanged] subscribeNext:^(NSString *x) {
bself.viewModel.playerName = x;
}];
The first control is the UITextField for the player name, so we need to bind its text property to the view models playerName property. We also need to make sure we update the view model with updates from the text field. This ensures that the text field receives any updates from the view model and also makes sure that the view is updating the player name property on the view model.
//the score property is a double, RC gives us updates as NSNumber which we just call
//stringValue on and bind that to the scorefield text
RAC(self.scoreField.text) = [RACObserve(self.viewModel.points) map:^id(NSNumber *value) {
return [value stringValue];
}];
We need to bind the labels text property to the view models points property. The view models points property is a double, but ReactiveCocoa gives us updates to this as NSNumber's so we need to map this to a NSString which the label can use.
//Setup bind the steppers values
self.scoreStepper.value = self.viewModel.points;
RAC(self.scoreStepper.stepValue) = RACObserve(self.viewModel,stepAmount);
RAC(self.scoreStepper.maximumValue) = RACObserve(self.viewModel,maxPoints);
RAC(self.scoreStepper.minimumValue) = RACObserve(self.viewModel,minPoints);
Simple binding and setup. Give the stepper the initial value from the view model and bind the properties (stepValue,min,max) to the view model.
//bind the hidden field to a signal keeping track if
//we've updated less than a certain number times as the view model specifies
RAC(self.scoreStepper.hidden) = [RACObserve(self,scoreUpdates) map:^id(NSNumber *x) {
return @(x.intValue >= bself.viewModel.maxPointUpdates);
}];
//only take the maxPointUpdates number of score updates
[[RACObserve(self.scoreStepper,value) take:self.viewModel.maxPointUpdates] subscribeNext:^(id newPoints) {
bself.viewModel.points = [newPoints doubleValue];
bself.scoreUpdates++;
}];
We'll enforce a rule from the view model that at any given time you can only update the points 10 times. To do this I created a NSUInteger property (in fact the only property we ever actually add to the view controller.) Then bind the hidden property to a signal which reduces to a 0 or 1 based on if the number of times we've updated is greater or equal to the number of times we've updated the points. So as soon as it hits the number we want this will return 1 and flip the hidden property to YES. To update the points we subscribe to the value property of the stepper and anytime it updates we update the view model and increment the player points.
//this signal should only trigger if we have "bad words" in our name
[self.viewModel.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];
bself.viewModel.playerName = @"";
}];
If you remember the view model has a validation signal which indicates if any names given are forbidden. My list is really bad and not representative of what I personally think are bad words by any stretch of the imagination. The beauty of this is that we don't need to know any of the details of what the view model thinks is a "bad word", but it can just provide a signal to indicate that it thinks something should be forbidden. The view model can provide signals for many things like this that it thinks are important to let you know about it. In this case all we've decided to do is show an alert view and reset the player name.
//let the upload(save) button only be enabled when the view model says its valid
RAC(self.uploadButton.enabled) = self.viewModel.modelIsValidSignal;
Another signal that the view model provides to us is a signal letting us know if the view model as it exists it valid or not. In this case i've hooked this signal to the enabled property of the upload button. So we should not be able to upload the player stats if the state of the view model is bad. The signal here is simple
-(RACSignal *)modelIsValidSignal {
@weakify(self):
return [RACSignal
combineLatest:@[ RACObserve(self,playerName), RACObserve(self,points) ]
reduce:^id(NSString *name, NSNumber *playerPoints){
@strongify(self);
return @((name.length > 0) && (![self.forbiddenNames containsObject:name]) && (playerPoints.doubleValue >= self.minPoints));
}];
}
So for the purposes of this simple demo, we are just concerned if the name length is greater than 0, the name isn't in the forbidden names and the points are greater than the minimum. Really we should be concerned with more, but I won't extend it further. We could even just put the forbidden names signal in if we changed it to use map and return a NSNumber with 0 or 1 and so we could use it here inside this signal.
//set the control action for our button to be the ViewModels action method
[self.uploadButton addTarget:self.viewModel
action:@selector(uploadData:)
forControlEvents:UIControlEventTouchUpInside];
The button is set to trigger the View Mode's upload data action. This method in the sample code simulates uploading something then presents an alert view letting you know about the success of uploading.
//we can subscribe to the same thing in multiple locations
//here we skip the first 4 signals and take only 1 update
//and then disable/hide certain UI elements as our app
//only allows 5 updates
[[[[self.uploadButton rac_signalForControlEvents:UIControlEventTouchUpInside]
skip:(kMaxUploads - 1)] take:1] subscribeNext:^(id x) {
bself.nameField.enabled = NO;
bself.scoreStepper.hidden = YES;
bself.uploadButton.hidden = YES;
}];
Here since there is already signals enabling the button now the number of uploads need to be limited. This is subscribing to the signal for the UIControlEventTouchUpInside event and on the nth update where n is kMaxUploads. On the nth update the namefield is disabled, and the score stepper and upload buttons are hidden.
All of this results in a UI that just binds its properties to a view model, sets its actions to actions on the view model and receives signals from the view model indicating things of interest. I admit this particular example is a bit weird, but it demonstrates using a view model and using the basic aspects of ReactiveCocoa. There is a lot more to ReactiveCocoa in particular that I haven't shown here for the sake of not introducing too many concepts all at once. Additionally we could build some better view models for dealing with networking code and could deal with child view controllers, but that will have to wait for another article.
So what have we seen here?
- MVVM stands for Model-View-ViewModel
- The View model contains things like properties to bind to, validation logic and actions to be done to the view model
- The View Model should not know anything about the UI that will bind to it
- ReactiveCocoa is an implementation of many of the API's in Microsofts Reactive Extensions for .NET in Cocoa
- We learned how to bind & subscribe to properties
- We learned how to create signals
The biggest thing you can see with reactive cocoa is that all the code and signals are just reacting to events. When we chain these responses together and filter events properly we heavily reduce the need to create tons of variables and methods just to keep track of the events going on. This leads to more reliable code and with the view model we can easily test it and simulate a UI without actually having a live UI in our app.
The complete source code thats shown here is located here https://github.com/Machx/MVVM-IOS-Example.
So check out ReactiveCocoa on github, and I've provided some helpful links at the bottom to help understand these concepts. If people want i'll dive deeper into ReactiveCocoa in later articles.
Update Since first publication this article has been updated to address a couple minor issues
- Make sure we don't retain self in blocks
- Update a piece of code where -map could simply be used in place of -combineLatest:reduce:
- The github sample code repo code has been updated