Introduction to MVVM

I got my first iOS job at 500px in 2011. I had been doing iOS contracting for a few years in college, but this was my first, real iOS gig. I was hired as the sole iOS developer to make the beautifully designed iPad app. In only seven weeks, we shipped a 1.0 and continued to iterate, adding more features and, intrinsically, more complexity to the codebase.

It felt at times like I didn't know what I was doing. I knew my design patterns – like any good coder – but I was way too close to the product I was making to objectively measure the efficacy of my architectural decisions. It took bringing another developer on board the team for me to realize that we were in trouble.

Ever heard of MVC? Massive View Controller, some call it. That's certainly how it felt at the time. I won't go into the embarrassing details, but it suffices to say that if I had to do it all over again, I would make different decisions.

One of the key architectural changes I would make, and have made in apps I've developed since then, would be to use an alternative to Model-View-Controller called Model-View-ViewModel.

So what is MVVM, exactly? Instead of focusing on the historical context of where MVVM came from, let's take a look at what a typical iOS app looks like and derive MVVM from there:

Here we see a typical MVC setup. Models represent data, views represent user interfaces, and view controllers mediate the interactions between the two of them. Cool.

Consider for a moment that, although views and view controllers are technically distinct components, they almost always go hand-in-hand together, paired. When is the last time that a view could be paired with different view controllers? Or vice versa? So why not formalize their connection?

This more accurately describes the MVC code that you're probably already writing. But it doesn't do much to address the massive view controllers that tend to grow in iOS apps. In typical MVC applications, a lot of logic gets placed in the view controller. Some of it belongs in the view controller, sure, but a lot of it is what's called 'presentation logic,' in MVVM terms -- things like transforming values from the model into something the view can present, like taking an NSDate and turning it into a formatted NSString.

We're missing something from our diagram. Something where we can place all of that presentation logic. We're going to call this the 'view model' – it will sit between the view/controller and the model:

Looking better! This diagram accurately describes what MVVM is: an augmented version of MVC where we formally connect the view and controller, and move presentation logic out of the controller and into a new object, the view model. MVVM sounds complicated, but it's essentially a dressed-up version of the MVC architecture that you're already familiar with.

So now that we know what MVVM is, why would one want to use it? The motivation behind MVVM on iOS, for me, anyway, is that it reduces the complexity of one's view controllers and makes one's presentation logic easier to test. We'll see how it accomplishes these goals with some examples.

There are three really important points I want you to take away from this article:

  • MVVM is compatible with your existing MVC architecture.

  • MVVM makes your apps more testable.

  • MVVM works best with a binding mechanism.

As we saw earlier, MVVM is basically just a spruced-up version of MVC, so it's easy to see how it can be incorporated into an existing app with a typical MVC architecture. Let's take a simple Person model and corresponding view controller:

								@interface Person : NSObject

- (instancetype)initwithSalutation:(NSString *)salutation firstName:(NSString *)firstName lastName:(NSString *)lastName birthdate:(NSDate *)birthdate;

@property (nonatomic, readonly) NSString *salutation;
@property (nonatomic, readonly) NSString *firstName;
@property (nonatomic, readonly) NSString *lastName;
@property (nonatomic, readonly) NSDate *birthdate;

@end

							

Cool. Now let's say that we have a PersonViewController that, in viewDidLoad, just sets some labels based on its model property:

								- (void)viewDidLoad {
    [super viewDidLoad];
    
    if (self.model.salutation.length > 0) {
        self.nameLabel.text = [NSString stringWithFormat:@"%@ %@ %@", self.model.salutation, self.model.firstName, self.model.lastName];
    } else {
        self.nameLabel.text = [NSString stringWithFormat:@"%@ %@", self.model.firstName, self.model.lastName];
    }
    
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"];
    self.birthdateLabel.text = [dateFormatter stringFromDate:model.birthdate];
}

							

That's all fairly straightforward, vanilla MVC. Now let's see how we can augment this with a view model:

								@interface PersonViewModel : NSObject

- (instancetype)initWithPerson:(Person *)person;

@property (nonatomic, readonly) Person *person;

@property (nonatomic, readonly) NSString *nameText;
@property (nonatomic, readonly) NSString *birthdateText;

@end

							

Our view model's implementation would look like the following:

								@implementation PersonViewModel

- (instancetype)initWithPerson:(Person *)person {
    self = [super init];
    if (!self) return nil;
    
    _person = person;
    if (person.salutation.length > 0) {
        _nameText = [NSString stringWithFormat:@"%@ %@ %@", self.person.salutation, self.person.firstName, self.person.lastName];
    } else {
        _nameText = [NSString stringWithFormat:@"%@ %@", self.person.firstName, self.person.lastName];
    }
    
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"];
    _birthdateText = [dateFormatter stringFromDate:person.birthdate];
    
    return self;
}

@end

							

Cool. We've moved the presentation logic in viewDidLoad into our view model. Our new viewDidLoad method is now very lightweight:

								- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.nameLabel.text = self.viewModel.nameText;
    self.birthdateLabel.text = self.viewModel.birthdateText;
}

							

So, as you can see, not a lot changed from our MVC architecture. It's the same code, just moved around. It's compatible with MVC, leads to lighter view controllers, and is more testable.

Testable, eh? How's that? Well, view controllers are notoriously hard to test since they do so much. In MVVM, we try and move as much of that code as possible into view models. Testing view controllers becomes a lot easier, since they're not doing a whole lot, and view models are very easy to test. Let's take a look:

								SpecBegin(Person)
    NSString *salutation = @"Dr.";
    NSString *firstName = @"first";
    NSString *lastName = @"last";
    NSDate *birthdate = [NSDate dateWithTimeIntervalSince1970:0];

    it (@"should use the salutation available. ", ^{
        Person *person = [[Person alloc] initWithSalutation:salutation firstName:firstName lastName:lastName birthdate:birthdate];
        PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person];
        expect(viewModel.nameText).to.equal(@"Dr. first last");
    });

    it (@"should not use an unavailable salutation. ", ^{
        Person *person = [[Person alloc] initWithSalutation:nil firstName:firstName lastName:lastName birthdate:birthdate];
        PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person];
        expect(viewModel.nameText).to.equal(@"first last");
    });

    it (@"should use the correct date format. ", ^{
        Person *person = [[Person alloc] initWithSalutation:nil firstName:firstName lastName:lastName birthdate:birthdate];
        PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person];
        expect(viewModel.birthdateText).to.equal(@"Thursday January 1, 1970");
    });
SpecEnd

							

If we hadn't moved this logic into the view model, we'd have had to instantiate a complete view controller and accompanying view, comparing the values inside our view's labels. Not only would that have been an inconvenient level of indirection, but it also would have represented a seriously fragile test. Now we're free to modify our view hierarchy at will without fear of breaking our unit tests. The testing benefits of using MVVM are clear, even from this simple example, and they become more apparent with more complex presentation logic.

Note that in this simple example, the model is immutable, so we can assign our view model's properties at initialization time. For mutable models, we'd need to use some kind of binding mechanism so that the view model can update its properties when the model backing those properties changes. Furthermore, once the models on the view model change, the views' properties need to be updated as well. A change from the model should cascade down through the view model into the view.

On OS X, one can use Cocoa bindings, but we don't have that luxury on iOS. Key-value observation comes to mind, and it does a great job. However, it's a lot of boilerplate for simple bindings, especially if there are lots of properties to bind to. Instead, I like to use ReactiveCocoa, but there's nothing forcing one to use ReactiveCocoa with MVVM. MVVM is a great paradigm that stands on its own and is only made better with a nice binding framework.

We've covered a lot: deriving MVVM from plain MVC, seeing how they're compatible paradigms, looking at MVVM from a testability perspective, and seeing that MVVM works best when paired with a binding mechanism. If you're interested in learning more about MVVM, you can check out this blog post explaining the benefits of MVVM in greater detail, or this article about how we used MVVM on a recent project of mine to great success. I also have a fully tested, MVVM-based app called C-41 that's open sourced. Check it out and let me know if you have any questions.