Skip to content

Instantly share code, notes, and snippets.

@IanKeen
Created September 15, 2015 16:14
Show Gist options
  • Save IanKeen/f585650b48040dec7d37 to your computer and use it in GitHub Desktop.
Save IanKeen/f585650b48040dec7d37 to your computer and use it in GitHub Desktop.
Don't let your UIViewController think for itself...

Don't let your UIViewController think for itself...

Last time I showed you one strategy for trimming the fat from our view controller by seperating out protocols. In part 2 of this 3 part series we are going to use an architecture known as MVVM to dumb down our view controller. View controllers are usually filled with business rules, this is a problem for a number of reasons, two of which being:

  • It makes the code harder to reason about; let the view controller worry about appearance and the business objects worry about the logic, we don't want them thinking for themselves ;)
  • It makes your view controller bloated; business rules don't belong in your view controller

MVVM ?

MVVM stands for Model View ViewModel and is just a way of describing how to break up your interface, business rules and data so that they flow together in a logical, but seperated, way.

This is what that flow looks like

A smart guy I know summed this up well...

"...data flows from the Model into the ViewModel which transforms it and pushes it to the View. User interactions and edits flow from the View to the ViewModel which transforms that into data which it sends to the Model"

I could ramble on and on about the benefits of adopting MVVM (testing, readability, etc..) but it's a little beyond the scope of this article. Fret not though! If you want to learn more (and you should!) the very smart guys over at objc.io already have a great article explaining it.

Feature Creep

So we have a pretty good view controller at this point.. and in a perfect world it would never have to change, but then as developers we would also be out of a job pretty quickly.

Back in the real world your product manager has just come to you with a great new feature people want. Joe Public desperately wants the text in the navigation bar to change to the title of the item that was last tapped in the list AND reset when the dataset is reloaded.

That's super easy right? Yup! We'll just go and write that code in the... umm... view controller??... NO! BAD DEVELOPER! DROP THE KEYBOARD... DROP IIITTT...

Good developer, the goal today is to get those business rules out of our view controller. So where will they live? I'm glad you asked... we are going to put them into our ViewModel.

ViewModel

If you already know about MVVM or you've gone and read the article provided above you know all about ViewModels. They are the object passed to a view controller in MVVM that contains any logic and events specific to that view controller, it's basically the brains of the operation.

Migrating to a ViewModel

Ok, so back in our project the first thing we need to do is create a ViewModel, they are just a simple NSObject so go ahead and create a new object called VCTableModel. Before we get started with our new features our view controller has some existing business rules that we can pull right out into our ViewModel, can you see it?... it's the MyAPI code! Let's handle that first by defining our ViewModel as follows:

//VCTableModel.h
@class MyAPI;

typedef void(^didReloadDataBlock)(NSArray *data);
typedef void(^didErrorBlock)(NSString *error);

@interface VCTableModel : NSObject
-(instancetype)initWithManager:(MyAPI *)api;
-(void)reloadData;
@property (nonatomic, copy) didReloadDataBlock didReloadData;
@property (nonatomic, copy) didErrorBlock didError;
@end

You can see here we are passing our MyAPI object to VCTableModel via its constructor. This technique is known as dependency injection and is a perfect match for MVVM. If you want to know more about dependency injection (again, you should!) the guys at objc.io come to the rescue again with their article on the subject.

One of the (many) huge benefits of MVVM is that, when used properly, you can make your view controller incredibly reactive to changes in the data it shows. This is accomplished by creating bindings between your view controller and the ViewModel.

There are a number of tools available for you to create these bindings however to keep things simple we are just going to use blocks. You can see we have defined two 'events' here, one to handle any errors we encounter and one when the data has been updated. Lastly we have a method to reload the data in the view controller.

Now that we have defined an interface let's start fleshing out the implementation, open up VCTableModel.m and create a private property for our MyAPI instance.

@property (nonatomic, strong) MyAPI *api;

Next add the constructor

-(instancetype)initWithManager:(MyAPI *)api {
    if (!(self = [super init])) { return nil; }
    self.api = api;
    return self;
}

Next up let's add the methods that will 'push' our events and a small helper to change an NSError into an NSString

-(NSString *)userMessageForError:(NSError *)error {
    return [NSString stringWithFormat:@"An Error Occured\n\n%@", error.localizedDescription];
}
-(void)sendError:(NSError *)error {
    if (self.didError == nil) { return; }
    
    dispatch_async(dispatch_get_main_queue(), ^{
        self.didError([self userMessageForError:error]);
    });
}
-(void)sendData:(NSArray *)data {
    if (self.didReloadData == nil) { return; }
    
    dispatch_async(dispatch_get_main_queue(), ^{
        self.didReloadData(data);
    });
}

Nice work! The only thing left to do is bring in the code that uses MyAPI we even get to thin it out because it's no longer directly coupled to the interface!

-(void)reloadData {
    [self.api getPhotos:^(NSArray *json) {
        //Convert JSON items into our model objects
        NSMutableArray *items = [NSMutableArray array];
        for (NSDictionary *jsonItem in json) {
            VCTableCellData *item = [[VCTableCellData alloc] initWithJSON:jsonItem];
            [items addObject:item];
        }
        
        //Raise didReloadData
        [self sendData:items];
        
    } error:^(NSError *error) {
        //Something went wrong, raise didError
        [self sendError:error];
    }];
}

That's it! the current features are now in a ViewModel ready to be handed to your view controller!

Migrating the View Controller

Out with the old

Now that we have created our ViewModel we need to update our view controller to use it. We will start by stripping out the code we have now moved.

Perform the following:

  • in viewDidLoad change Slim to MVVM in the title and remove self.api = [MyAPI new];
  • remove #import "MyAPI.h"
  • remove the private properties for MyAPI *api
  • remove the existing implementation of reloadData and replace it with:
-(void)reloadData {
    [self.refresh beginRefreshing];
    [self.model reloadData];
}

You can ignore the compiler error for now, we are about to fix it.

In with the new

First thing we need to do is provide an instance of VCTableModel to our view controller via the constructor:

//VCTable.h
@class VCTableModel;

@interface VCTable : UIViewController
-(instancetype)initWithModel:(VCTableModel *)model;
@end

Next in the implementation we add a private property for the ViewModel

#import "VCTableModel.h"

@property (nonatomic, strong) VCTableModel *model;

Then our new constructor

-(instancetype)initWithModel:(VCTableModel *)model {
    if (!(self = [super initWithNibName:NSStringFromClass([self class]) bundle:nil])) { return nil; }
    self.model = model;
    return self;
}

Nearly there! Next we add methods to handle each of the ViewModel's events as well as a method to hook it all up

-(void)bindToModel {
    self.model.didError = [self modelDidError];
    self.model.didReloadData = [self modelDidReloadData];
}
-(didErrorBlock)modelDidError {
    return ^(NSString *error) {
        [self endRefreshing];
        
        [[[UIAlertView alloc]
          initWithTitle:@"Oops..." message:error 
          delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]
         show];
    };
}
-(didReloadDataBlock)modelDidReloadData {
    return ^(NSArray *data) {
        [self endRefreshing];
        [self.tableViewCoordinator reloadData:data];
    };
}

Quick Note: You may be unfamiliar with the syntax of these event handling methods but blocks are actually objects so we can return them from a method. Writing them in this way just improves their readability.

Lastly make sure you call [self bindToModel]; in your viewDidLoad

Congratulations! You have converted your view controller to MVVM!

New Features!

Now we can get busy adding our new features! The first thing we should do is move the title of the view controller to our ViewModel, this will make it easy to reset and it's good practice. Imagine you have a profile view that's backed by a User object, you can easily set the title to the user's name while keeping your data hidden from the view controller (a key principle of MVVM).

So let's update our ViewModel:

//VCTableModel.h
@property (readonly) NSString *title;


//VCTableModel.m
-(NSString *)title {
    return @"MVVM Table View Example";
}

and in our view controller update the viewDidLoad with self.title = self.model.title;

We know our two new requirements both update the navigation bar title, we can accomplish that with a single new event:

//VCTableModel.h
typedef void(^didUpdateNavigationTitleBlock)(NSString *title);

@property (nonatomic, copy) didUpdateNavigationTitleBlock didUpdateNavigationTitle;

and we need a method we can call when the user has tapped a row in the table

//VCTableModel.h
@class VCTableCellData;
-(void)userSelectedCell:(VCTableCellData *)cellData;


//VCTableModel.m
-(void)userSelectedCell:(VCTableCellData *)cellData {
    [self sendNavigationTitle:cellData.title];
}

The last change in our ViewModel is to update reloadData to call [self sendNavigationTitle:self.title]; before making the api call.

Finally in our view controller we simply add a method to handle the new event:

-(didUpdateNavigationTitleBlock)modelDidUpdateNavigationTitle {
    return ^(NSString *title) {
        self.title = title;
    };
}

Then add self.model.didUpdateNavigationTitle = [self modelDidUpdateNavigationTitle]; to bindToModel

Lastly add [self.model userSelectedCell:data]; to itemPressed: so that our model can fire these new events and your new features are done!!

Finishing Touches

One last step I like to do is make a convenience method that makes it super easy to bring together the view controller, its ViewModel and the dependencies.

Make a new category/extension on VCTable called VCTable+Factory

//VCTable+Factory.h
@interface VCTable (Factory)
+(instancetype)factoryInstance;
@end


//VCTable+Factory.m
#import "VCTableModel.h"
#import "MyAPI.h"

@implementation VCTable (Factory)
+(instancetype)factoryInstance {
    VCTableModel *model = [[VCTableModel alloc] initWithManager:[MyAPI new]];
    return [[VCTable alloc] initWithModel:model];
}
@end

In your AppDelegate change #import "VCTable.h" to #import "VCTable+Factory.h" and update application:didFinishLaunchingWithOptions: to:

-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    
    VCTable *rootViewController = [VCTable factoryInstance];
    self.navigationController = [[UINavigationController alloc] initWithRootViewController:rootViewController];
    self.window.rootViewController = self.navigationController;
    [self.window makeKeyAndVisible];
    
    return YES;
}

Now you can take it for a spin!

Slim, silly view controller!

Nice work! Now our view controller doesn't need to worry about anything other than showing what it's told, when it's told!

I hope this intro into MVVM was fairly simple and I've shown you how easy it is to break your business rules out of your view controllers.

Grab the source for our MVVM view controller here.

In the final part of this series we will address that god awful scrolling performance! Stay tuned!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment