Standard architecture of iOS apps is usually sliced into 3 layers: model, view and controller. Model-View-Controller pattern is really straightforward and elegant: view delegates user actions to controller, controller updates model, and (vice versa) model notifies controller when change occurs and controller updates the view.

MVC architecture

The easiest way to understand this is if you think of a table view that loads data from fetched results controller. Table view and fetched results controller represent view and controller layers respectively and fetched core data model objects represent model.

The problem with standard Cocoa MVC is that view and controller layers are so tightly coupled that they live in the same class (hence the Cocoa naming convention: UIViewController). And, as you probably learned with time, Model-ViewController pattern has some disadvantages:

  1. Since they define all UI logic, map models to presentable information, respond to notifications and handle all user actions, view controllers will grow really big really fast and you’ll end up with unsustainable, unreadable, untestable classes that no one will ever want to use again.
  2. Testing application logic is often impossible without mocking view controllers since most of the code is in them (or even views). I know a lot of people who simply stopped testing because of tight coupling and inability to blackbox features (which is probably better than mocking entire context to test some functionality, anyway).M_VC

What we need is an extra layer between model and view as defined by Model-View-ViewModel (MVVM) pattern. MVVM improves testability, sustainability, readability, reusability and all the other -ilities we, developers, love and cherish.

While model and view layers have same responsibilities as defined by MVC pattern, viewModel is a new layer: a class owned by view where all the presentation logic and user actions are defined. It is totally unaware of the view that’s using it and is a simple subclass of NSObject.View model observes changes on a model, processes information and notifies its owner that information is ready to be consumed either by sending signals (reactive pattern) or updating value for key (observer pattern).

MVVM

To see the awesomeness of MVVM in action, let’s start writing a really simple iOS app that helps us track if we fed our pets (a really useful one, indeed) using MVVM and ReactiveCocoa (If you live on Tatooine and Jawas stole your Internet equipment so you haven’t heard about ReactiveCocoa yet, check out their flashy Github repo).

Pet Feeder

Let’s start by creating an app model controller. This NSObject subclass exposes methods like:

  • addNewPetWithName:
  • updateImage:forPetAtIndex:
  • updateName:forPetAtIndex:
  • modelAtIndex:
  • removeObjectAtIndex:

Methods removeObjectAtIndex: and modelAtIndex: don’t have such specific names (they don’t mention any pets, right?) because they are defined as a part of the the more abstract SMLStandardTableViewModel protocol.

Our first view is an UITableView subclass where users can add, edit and remove pets. So, when setting viewModel for our table view, (table) view needs to subscribe to all the changes it’s interested in:


//  SMLPetsTableViewController.m
//  SMLPetFeeder

- (void)setupModelController {
    self.viewModel = [[SMLAppModelController alloc] init];
    @weakify(self);
    [self.viewModel.addedPet subscribeNext:^(NSNumber *index) {
    @strongify(self);
    NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index.integerValue inSection:0];
    [self.tableView insertRowsAtIndexPaths:@[indexPath]
                          withRowAnimation:UITableViewRowAnimationMiddle];
}];

[self.viewModel.removedPet subscribeNext:^(NSNumber *index) {
    @strongify(self);
    NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index.integerValue inSection:0];
    [self.tableView deleteRowsAtIndexPaths:@[indexPath]
                          withRowAnimation:UITableViewRowAnimationMiddle];
}];
}

@weakify and @strongify are a part of the libextobjc library and they provide a very elegant solution for avoiding retain cycles in blocks. Check out libextobjc GitHub repo here.

When table view is loading cells, all we need to do to set up a cell inside tableView:cellForRowAtIndexPath: is setting a viewModel for that cell:


- (SMLStandardTableViewCell*)tableView:(UITableView*)tableView
                 cellForRowAtIndexPath:(NSIndexPath*)indexPath {
    SMLStandardTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"TableCell"];
    cell.cellModel = [self.viewModel modelAtIndex:indexPath.row];
    return cell;
}

SMLBasicCellModel is a protocol where readonly properties like title, subtitle and image are defined.

When pet name or pet image changes, we don’t want to reload the whole table view (to avoid flashes and/or other unneeded animations) so, in a table view cell setCellModel: method, we make sure that cell subscribes to all the changes it is interested in:


//  SMLStandardTableViewCell.m
//  SMLPetFeeder

- (void)setCellModel:(id<SMLBasicCellModel>)cellModel {
    _cellModel = cellModel;
    @weakify(self)
    [self.cellModel.updatedTitle subscribeNext:^(NSString *title) {
        @strongify(self)
        self.titleLabel.text = title;
    }];
    self.titleLabel.text = self.cellModel.title;
    [self.cellModel.updatedImage subscribeNext:^(UIImage *image) {
        @strongify(self)
        self.roundImageView.image = image;
    }];
    self.roundImageView.image = self.cellModel.image;
}

Let’s say a user is trying to remove a pet. ViewModel is responsible for updating the model and informing the view (sending reactive signal) about it:


//  SMLAppModelController.m
//  SMLPetFeeder

- (void)removeObjectAtIndex:(NSUInteger)index {
    SMLPetViewModel *petModel = [self modelAtIndex:index];
    if (!petModel) {
        NSLog(@"Error: no pet at index %zd", index);
    }
    [self.petModels removeObject:petModel];
    [self.dataController removePet:petModel.pet];
    [self.removedPet sendNext:@(index)];
}

Isn’t that nice? 🙂 Let’s go ahead and review what we just did:

  1. Create and assign viewModel to a view
  2. Propagate all the user actions to viewModel
  3. Update model and notify view about it
  4. React to model changes by updating part of the view that shows updated model

Simple and elegant, isn’t it? Let’s see how we test our viewModels.

Tests

All pet objects have ordinal values associated to them so we can keep them sorted in the same order they were created. Let’s write a really simple test that adds some pets, removes one of them, then adds few more and checks if their ordinals are updated as expected.

To test MVC viewControllers, you usually had to stub lots of UI code in setUp/tearDown methods. With MVVM, viewModel is a subclass of NSObject so you can simply test its instance:


//  PetFeederTests.m
//  PetFeederTests

- (void)setUp {
    [super setUp];
    self.appModelController = [[SMLAppModelController alloc] init];
}

- (void)testOrdinals {
    [self.appModelController addNewPetWithName:@"Lady"];
    [self.appModelController addNewPetWithName:@"Tramp"];
    [self.appModelController addNewPetWithName:@"Beethoven"];
    [self.appModelController addNewPetWithName:@"Garfield"];

    [self ensureOrdinalsUpdated];

    [self.appModelController removeObjectAtIndex:0];

    [self ensureOrdinalsUpdated];

    [self.appModelController addNewPetWithName:@"Lady"];
    [self.appModelController addNewPetWithName:@"Marino"];

    [self ensureOrdinalsUpdated];
}

- (void)ensureOrdinalsUpdated {
    for (NSInteger i=0; i<self.appModelController.count; i++) {
        SMLPetViewModel *petViewModel = [self.appModelController modelAtIndex:i];
        XCTAssert(petViewModel.pet.ordinal.integerValue == i, @"Wrong ordinal");
    }
}

Most of the time (like in this example) you won’t even need to mock anything and your tests will be really consistent.

As your project grows and you keep typing new features, you’ll really start to appreciate the upsides of MVVM. If you want to learn more about MVVM and good architecture design in general, Introduction to MVVM by Ash Furrow and entire objc.io Issue #13 is my ultimate recommendation.

This example is available on GitHub.

0 comments