MVVM Tutorial with ReactiveCocoa: Part 2/2

Colin Eberhardt

Model-View-ViewModel (MVVM) is a UI design pattern that is becoming a popular alternative to Model-View-Controller (MVC).

In the first part of this MVVM tutorial series, you saw how ReactiveCocoa forms the ‘glue’ that binds ViewModels to their respective Views:

MVVMReactiveCocoa

Here’s the application you’re building, it’s a Flickr search app:

FinishedApp

In this second part and final part of this MVVM tutorial series, you’re going to look at how you can ‘drive’ the navigation between view controllers from the application’s ViewModel.

So far, the application you’ve developed so far allows you to search Flickr with a simple search string. If you need a copy of current project, download it here.

A Model layer service that uses ReactiveCocoa supplies the search results, and the ViewModel simply logs the response.

Now, it’s time to work out how to navigate to the results page!

Implementing ViewModel Navigation

When a Flickr search successfully returns the desired behavior, the application navigates to a new view controller that displays the search results.

The current application has a single ViewModel, described by the RWTFlickrSearchViewModel class. In order to implement this desired behavior, you’re going add a new ViewModel to ‘back’ the search results View.

Add a new NSObject subclass named RWTSearchResultsViewModel to the ViewModel group. Update the header as follows:

@import Foundation;
#import "RWTViewModelServices.h"
#import "RWTFlickrSearchResults.h"
 
@interface RWTSearchResultsViewModel : NSObject
 
- (instancetype)initWithSearchResults:(RWTFlickrSearchResults *)results services:(id<RWTViewModelServices>)services;
 
@property (strong, nonatomic) NSString *title;
@property (strong, nonatomic) NSArray *searchResults;
 
@end

The above adds a couple of properties that describe the View, and an initializer constructed from the RWTFlickrSearchResults model object (which is returned by the Model layer service).

Open RWTSearchResultsViewModel.m and implement the initializer as follows:

- (instancetype)initWithSearchResults:(RWTFlickrSearchResults *)results services:(id<RWTViewModelServices>)services {
  if (self = [super init]) {
    _title = results.searchString;
    _searchResults = results.photos;
  }
  return self;
}

This completes RWTSearchResultsViewModel.

If you’ll recall from part one, the ViewModels are constructed before their View counterparts to ‘drive’ the application. The next step is to wire the ‘passive’ View to its respective ViewModel:

Open RWTSearchResultsViewController.h, import the ViewModel, and add an initializer as shown below:

#import "RWTSearchResultsViewModel.h"
 
@interface RWTSearchResultsViewController : UIViewController
 
- (instancetype)initWithViewModel:(RWTSearchResultsViewModel *)viewModel;
 
@end

Open RWTSearchResultsViewController.m and add the following private property to the class extension at the top of the file:

@property (strong, nonatomic) RWTSearchResultsViewModel *viewModel;

Further down the same file, implement the initializer:

- (instancetype)initWithViewModel:(RWTSearchResultsViewModel *)viewModel {
  if (self = [super init]) {
    _viewModel = viewModel;
  }
  return self;
}

In this step, you’re focusing on how navigation works. You’ll return to this view controller shortly to bind the ViewModel to the UI.

Your application now has two ViewModels, but you’re facing a conundrum here! How do you navigate from one ViewModel to the other, while also navigating between their respective view controllers?

The ViewModel cannot have a direct reference to the View, so what kind of crazy magic do you need to perform?

The answer is already present in the RWTViewModelServices protocol. It’s currently used to obtain a reference to the Model layer, and now you’re going to use this same protocol to allow the ViewModel to initiate the navigation.

Open RWTViewModelServices.h and add the following method to the protocol:

- (void)pushViewModel:(id)viewModel;

Conceptually, the ViewModel layer drives the application; logic within this layer determines what displays in the View, as well as how and when navigation should occur.

This method allows the ViewModel layer to initiate navigation by ‘pushing’ a ViewModel in the same way that a UINavigationController allows you to navigate by ‘pushing’ a UIViewController.

Before updating this procotol implementation, you’re going to put it to work within the ViewModel layer.

Open RWTFlickrSearchViewModel.m and import the newly added ViewModel:

#import "RWTSearchResultsViewModel.h"

Further down the same file, update the implementation of executeSearchSignal as follows:

- (RACSignal *)executeSearchSignal {
  return [[[self.services getFlickrSearchService]
    flickrSearchSignal:self.searchText]
    doNext:^(id result) {
      RWTSearchResultsViewModel *resultsViewModel =
        [[RWTSearchResultsViewModel alloc] initWithSearchResults:result services:self.services];
      [self.services pushViewModel:resultsViewModel];
    }];
}

The above adds a doNext operation to the signal the search command creates when it executes. The doNext block creates the new ViewModel that displays the search results, and then pushes it via the ViewModel-services.

Now, it’s time to update the code that implements this protocol, so when a ViewModel is pushed it navigates to the required view controller. To do this, the code needs a reference to the navigation controller.

Open RWTViewModelServicesImpl.h and add the following initializer:

- (instancetype)initWithNavigationController:(UINavigationController *)navigationController;

Open RWTViewModelServicesImpl.m and import the following header:

#import "RWTSearchResultsViewController.h"

A little further down, add the following private property:

@property (weak, nonatomic) UINavigationController *navigationController;

Further down the same file, update the initializer as follows:

- (instancetype)initWithNavigationController:(UINavigationController *)navigationController {
  if (self = [super init]) {
    _searchService = [RWTFlickrSearchImpl new];
    _navigationController = navigationController;
  }
  return self;
}

This simply updates the initializer to store a reference to the passed navigation controller.

Finally, add the new method:

- (void)pushViewModel:(id)viewModel {
  id viewController;
 
  if ([viewModel isKindOfClass:RWTSearchResultsViewModel.class]) {
    viewController = [[RWTSearchResultsViewController alloc] initWithViewModel:viewModel];
  } else {
    NSLog(@"an unknown ViewModel was pushed!");
  }
 
  [self.navigationController pushViewController:viewController animated:YES];
}

The above method uses this type of the supplied ViewModel to determine which View is required.

In the trivial case above, there is only one concrete ViewModel-View pair, but I’m sure you can see how you could extend this pattern. The navigation controller pushes the resulting View.

Onto the final step; open RWTAppDelegate.m, locate the line within createInitialViewController where the RWTViewModelServicesImpl instance is created, and update the code to pass the navigation controller to the initializer as follows:

self.viewModelServices = [[RWTViewModelServicesImpl alloc] initWithNavigationController:self.navigationController];

Build, run, type in a search term, hit ‘Go’ and watch as the application transitions to the new ViewModel / View:

BlankView

…which is currently blank! Don’t be disheartened; you’ll fix that in a few moments.

Instead, give yourself a pat on the back; you now have an application with multiple ViewModels where the navigation is entirely controller via the ViewModel-layer!

Note: John Gossman, one of the Microsoft developers who worked on WPF and helped create the MVVM pattern, often says that a litmus test for MVVM is whether the application runs without the UI.

Your application passes this test. If you’re feeling adventurous, prove it by writing a unit test that executes a search and navigates from one ViewModel to the next.

Now that you have a ‘pure’ solution, it’s time to get back to UI binding!

Rendering the Results Table

The View for the search results, as defined in RWTSearchResultsViewController, has a UITableView defined within its corresponding nib file. The next step is to render the ViewModel contents within this table.

Open RWTSearchResultsViewController.m and locate the class extension. Update it to implement the UITableViewDataSource protocol:

@interface RWTSearchResultsViewController () <UITableViewDataSource>

Further down the same file, override viewDidLoad as follows:

- (void)viewDidLoad {
  [super viewDidLoad];
 
  [self.searchResultsTable registerClass:UITableViewCell.class
                  forCellReuseIdentifier:@"cell"];
  self.searchResultsTable.dataSource = self;
 
  [self bindViewModel];
}

This performs the one-time initialization of the table view and binds the view model. Please ignore the hard-coded cell identifier constant, this will be removed shortly.

Further down the same file, add the bindViewModel method:

- (void)bindViewModel {
  self.title = self.viewModel.title;
}

Right now, this doesn’t do much. The ViewModel has two properties: the title that the above code handles, and the searchResults array that will render within the table.

So how do you bind this array to the table view? Unfortunately the answer is, you don’t. Huh, what??

ReactiveCocoa can bind simple properties on UIKit controls, but can’t handle the more complex interactions required to feed data into a table view :[ I know, it’s kind of a buzzkill.

There’s no need to panic, because there’s another way. It’s time to roll-up your sleeves and take the manual approach.

Within the same file, add the two mandatory data source methods as follows:

- (NSInteger)tableView:(UITableView *)tableView
 numberOfRowsInSection:(NSInteger)section {
  return self.viewModel.searchResults.count;
}
 
- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
  cell.textLabel.text = [self.viewModel.searchResults[indexPath.row] title];
  return cell;
}

The first simply reports the number of search results, while the second dequeues a cell and sets its title based on data from the ViewModel. That was simple enough wasn’t it?

Build and run to see your table now populated with data:

PopulatedTable

Better Table View Binding

The lack of table view binding can quickly result in a lean view controller growing in size. Manually ‘binding’ it to the table view makes this approach less elegant.

This problem bugged me (As it should!), so I set about solving it.

Conceptually, each item within the ViewModel’s searchResults array is a ViewModel in its own right, with each cell being the View for its respective ViewModel instance.

In a recent blog post I created a generic binding-helper, named CETableViewBindingHelper, that allows you to define the View used for each child ViewModel, with the helper taking care of implementing the datasource protocol. You’ll find this helper in the Util group of the current project.

The class constructor for CETableViewBindingHelper is shown below:

+ (instancetype) bindingHelperForTableView:(UITableView *)tableView
                              sourceSignal:(RACSignal *)source
                          selectionCommand:(RACCommand *)selection
                              templateCell:(UINib *)templateCellNib;

To bind an array to the view, you simply create an instance of the helper class. The arguments are:

  1. The table view which renders the array of ViewModels
  2. A source signal that relays changes to the array
  3. An optional command to execute when a row is selected
  4. The nib for the cell Views.

The cell that the given nib file defines within must implement the CEReactiveView protocol.

The project already contains a table view cell that you can use for rendering the search results. Open RWTSearchResultsTableViewCell.h and import the required protocol:

#import "CEReactiveView.h"

And adopt it:

@interface RWTSearchResultsTableViewCell : UITableViewCell <CEReactiveView>

The next step is to implement this protocol. Open RWTSearchResultsTableViewCell.m and add the following imports:

#import <SDWebImage/UIImageView+WebCache.h>
#import "RWTFlickrPhoto.h"

Then add the following method:

- (void)bindViewModel:(id)viewModel {
  RWTFlickrPhoto *photo = viewModel;
  self.titleLabel.text = photo.title;
 
  self.imageThumbnailView.contentMode = UIViewContentModeScaleToFill;
 
  [self.imageThumbnailView setImageWithURL:photo.url];
}

Currently the searchResults property of RWTSearchResultsViewModel contains an array of RWTFlickrPhoto instances. Rather than wrapping these Model objects in ViewModels, they are bound directly to the view.

Note: In instances where you don’t need any View-related logic, there’s nothing wrong with exposing Model objects to your View. Keep it simple!

The bindViewModel method you just added also makes use of the SDWebImage pod that was added via CocoaPods. This useful utility downloads and decodes images on background threads, greatly improving scroll performance.

The setImageWithURL: method on UIImageView is a category method added by SDWebImage.

The final step is to use the binding helper to render the table.

Open RWTSearchResultsViewController.m and import the helper:

#import "CETableViewBindingHelper.h"

Further down the same file, remove the UITableDataSource protocol implementation. You may also remove the implementation of the two methods from this protocol.

Next, add the following private property within the class extension:

@property (strong, nonatomic) CETableViewBindingHelper *bindingHelper;

Further down the same file, remove the code you added in viewDidLoad to configure the table view and return this method to its original form:

- (void)viewDidLoad {
  [super viewDidLoad]; 
  [self bindViewModel];
}

Finally, add the following to the end of bindViewModel:

UINib *nib = [UINib nibWithNibName:@"RWTSearchResultsTableViewCell" bundle:nil];
 
self.bindingHelper =
  [CETableViewBindingHelper bindingHelperForTableView:self.searchResultsTable
                                         sourceSignal:RACObserve(self.viewModel, searchResults)
                                     selectionCommand:nil
                                         templateCell:nib];

This creates a UINib instance from the nib file and constructs the binding helper. The sourceSignal is created by observing changes to the searchResults property of the ViewModel.

Build, run and admire the new UI:

UsingTheBindingHelper

This is a much more elegant way to bind arrays to table views!

Some UI Flair

So far, this tutorial has focused on structuring your application to follow the MVVM pattern. I bet you’re ready to take a break and add some flair!

Since the release of iOS7, just over a year ago, ‘motion design‘ has gained greater visibility, and many designers now favor subtle animations and fluid actions.

In this step, you’re going to add a subtle parallax scrolling effect to the photos. Fancy!

Open RWTSearchResultsTableViewCell.h and add the following method:

- (void) setParallax:(CGFloat)value;

The table view that hosts these cells will use this to provide each cell with its parallax offset.

Open RWTSearchResultsTableViewCell.m and implement the following:

- (void)setParallax:(CGFloat)value {
  self.imageThumbnailView.transform = CGAffineTransformMakeTranslation(0, value);
}

That’s a nice, easy, simple transform.

Open RWTSearchResultsViewController.m and import the following header:

#import "RWTSearchResultsTableViewCell.h"

A little further down the same file, adopt the UITableViewDelegate protocol via the class extension:

@interface RWTSearchResultsViewController () <UITableViewDataSource, UITableViewDelegate>

You just added a binding helper that sets itself as the delegate for the table view so it can respond to row selection. However, it also forwards delegate method invocations to its own delegate property so you can still add a custom behavior.

Within the bindViewModel method, set the binding helper delegate:

self.bindingHelper.delegate = self;

Further down the same file, add an implementation of scrollViewDidScroll:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
  NSArray *cells = [self.searchResultsTable visibleCells];
  for (RWTSearchResultsTableViewCell *cell in cells) {
    CGFloat value = -40 + (cell.frame.origin.y - self.searchResultsTable.contentOffset.y) / 5;
    [cell setParallax:value];
  }
}

Each time the table view scrolls, it calls this method. It iterates over all the visible cells, computing an offset value that applies a parallax effect. The actual offset applied to each cell depends on its location within the visible portion of the table view.

The ’40’ and ‘5’ in this code are magic numbers, and I am sure some of you’ll treat them with a mixture of disgust and contempt. I’m not going to lie, I found suitable values by a process of trial and error!

Build, run and enjoy the parallax scrolling!

ParallaxAnimation

Back to the business of Views and ViewModels…

Querying for Comment and Favorite Count

The interface should show the number of comments and favorites for each photo on the bottom right. However, it simply shows the dummy text ‘123’ from the nib file at the moment.

You need to add this functionality to the Model layer before you can replace these with the real value. Follow the same process as before to add a Model object that represents the result of querying the Flickr API.

Add a new NSObject subclass called RWTFlickrPhotoMetadata to the Model group. Open RWTFlickrPhotoMetadata.h and add the following properties:

@property (nonatomic) NSUInteger favorites;
@property (nonatomic) NSUInteger comments;

Then open RWTFlickrPhotoMetadata.m and add and implement the description method:

- (NSString *)description {
  return [NSString stringWithFormat:@"metadata: comments=%lU, faves=%lU",
          self.comments, self.favorites];
}

This tests the newly added method for obtaining image metadata on the first photo in the returned results. The output of this signal is just logged.

Build, run and search for some photos. When the results display, you’ll see a log message similar to the one below:

2014-06-04 07:27:26.813 RWTFlickrSearch[76828:70b] metadata: comments=120, faves=434

Fetching Metadata for Visible Cells

You could extend on the current code to fetch the metadata for all the search results.

However, with 100 photos in a typical result, this would immediately fire 200 API requests, or two per photo. Most APIs have a rate limit, and this kind of usage is almost certainly going to get your API key blocked, at least temporarily.

You only need to fetch metadata for the photos that are currently visible within the table. So how do you implement this behavior? You guessed it, you need a ViewModel that is aware of its own visibility!

Currently RWTSearchResultsViewModel exposes an array of RWTFlickrPhoto instances that are bound to the View, and these are Model-layer objects that are exposed to the View. In order to add the concept of visibility, you’re going to wrap these model objects in ViewModels that add this View-centric state.

Within the ViewModel group add an NSObject subclass called RWTSearchResultsItemViewModel. Open the newly added header file and update as follows:

@import Foundation;
#import "RWTFlickrPhoto.h"
#import "RWTViewModelServices.h"
 
@interface RWTSearchResultsItemViewModel : NSObject
 
- (instancetype) initWithPhoto:(RWTFlickrPhoto *)photo services:(id<RWTViewModelServices>)services;
 
@property (nonatomic) BOOL isVisible;
@property (strong, nonatomic) NSString *title;
@property (strong, nonatomic) NSURL *url;
@property (strong, nonatomic) NSNumber *favorites;
@property (strong, nonatomic) NSNumber *comments;
 
@end

As you can see from the initializer, this ViewModel wraps an instance of the RWTFlickrPhoto model object.

The properties of this ViewModel are a mixture of:

  • Those which expose the underlying Model properties (title, url)
  • Those that update dynamically when the metadata is fetched (favorites, comments)
  • And isVisible that indicates whether this ViewModel is visible or not.

Open RWTSearchResultsItemViewModel.m and import the following headers:

#import <ReactiveCocoa/ReactiveCocoa.h>
#import <ReactiveCocoa/RACEXTScope.h>
#import "RWTFlickrPhotoMetadata.h"

Below, the imports add a class extension with a private property:

@interface RWTSearchResultsItemViewModel ()
 
@property (weak, nonatomic) id<RWTViewModelServices> services;
@property (strong, nonatomic) RWTFlickrPhoto *photo;
 
@end

Further down the same file, implement the initializer as follows:

- (instancetype)initWithPhoto:(RWTFlickrPhoto *)photo services:(id<RWTViewModelServices>)services {
  self = [super init];
  if (self) {
    _title = photo.title;
    _url = photo.url;
    _services = services;
    _photo = photo;
 
    [self initialize];
  }
  return  self;
}

This bases the title and url properties on the values from the Model object, and then stores reference to the services and photo arguments via the private properties.

Next add the initialize method. Get ready, this is where the clever stuff happens!

- (void)initialize {
  RACSignal *fetchMetadata =
    [RACObserve(self, isVisible)
     filter:^BOOL(NSNumber *visible) {
       return [visible boolValue];
     }];
 
  @weakify(self)
  [fetchMetadata subscribeNext:^(id x) {
    @strongify(self)
    [[[self.services getFlickrSearchService] flickrImageMetadata:self.photo.identifier]
     subscribeNext:^(RWTFlickrPhotoMetadata *x) {
       self.favorites = @(x.favorites);
       self.comments = @(x.comments);
     }];
  }];
}

The first part of this method creates a signal named fetchMetadata by observing the isVisible property and filtering for ‘true’ values. As a result, this signal emits a next value when the isVisible property is set to true.

The next part subscribes to this signal in order to initiate the request to the flickrImageMetadata method. When this nested signal fires a next event, the favorites and comments properties update with the result.

In summary, when isVisible is set to true, the Flickr API requests are fired, and the comments and favorites properties update at some point in the future.

To put this new ViewModel to use, open RWTSearchResultsViewModel.m and import the following:

#import <LinqToObjectiveC/NSArray+LinqExtensions.h>
#import "RWTSearchResultsItemViewModel.h"

Within the initializer, remove the code that currently sets _searchResults and replace with the following:

_searchResults =
  [results.photos linq_select:^id(RWTFlickrPhoto *photo) {
    return [[RWTSearchResultsItemViewModel alloc]
              initWithPhoto:photo services:services];
  }];

This simply wraps each Model object with a ViewModel.

The final step is to set the isVisible property via the View and to make use of these new properties.

Open RWTSearchResultsTableViewCell.m and add the following import:

#import "RWTSearchResultsItemViewModel.h"

Further down the same file, change the first line of the bindViewModel method to use the newly added ViewModel:

RWTSearchResultsItemViewModel *photo = viewModel;

Further down the same method, add the following:

[RACObserve(photo, favorites) subscribeNext:^(NSNumber *x) {
  self.favouritesLabel.text = [x stringValue];
  self.favouritesIcon.hidden = (x == nil);
}];
 
[RACObserve(photo, comments) subscribeNext:^(NSNumber *x) {
  self.commentsLabel.text = [x stringValue];
  self.commentsIcon.hidden = (x == nil);
}];
 
photo.isVisible = YES;

This observes the new comments and favorites properties, when they are updated the corresponding labels and images are updated.

Finally, the isVisible property of the ViewModel is set to YES.

Colin Eberhardt

Colin Eberhardt has been writing code and tutorials for many years, covering a wide range of technologies and platforms. Most recently he has turned his attention to iOS. Colin is CTO of ShinobiControls, creators of charts, grids and other powerful iOS controls.

You can check out their app, ShinobiPlay, in the App Store.

Other Items of Interest

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Come check out Alt U

Our Books

Our Team

Video Team

... 9 total!

Swift Team

... 15 total!

iOS Team

... 47 total!

Android Team

... 15 total!

OS X Team

... 12 total!

Apple Game Frameworks Team

... 15 total!

Unity Team

... 11 total!

Articles Team

... 8 total!