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:
Here’s the application you’re building, it’s a Flickr search app:
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:
…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:
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:
- The table view which renders the array of ViewModels
- A source signal that relays changes to the array
- An optional command to execute when a row is selected
- 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:
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!
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 will be useful for testing the functionality before the View-layer changes complete.
Next open the RWTFlickrSearch.h and add the following method to the protocol:
- (RACSignal *)flickrImageMetadata:(NSString *)photoId; |
ViewModel will use this to request the metadata for a given photo, like comments and favorites.
Now, move on to the implementation! Open RWTFlickrSearchImpl.m and add the following imports:
#import "RWTFlickrPhotoMetadata.h" #import <ReactiveCocoa/RACEXTScope.h> |
The next step is to implement the flickrImageMetadata
method.
Unfortunately, there is a bit of a problem: in order to obtain the number of comments related to a photo, you need to call the flickr.photos.getInfo
API method; to obtain the number of favorites you need to call the flickr.photos.getFavorites
method.
That complicates matters a bit, with the flickrImageMetadata
method requiring two API requests in order to provide the required data.
Fortunately, ReactiveCocoa makes this very easy!
Add the following method implementation:
- (RACSignal *)flickrImageMetadata:(NSString *)photoId { RACSignal *favorites = [self signalFromAPIMethod:@"flickr.photos.getFavorites" arguments:@{@"photo_id": photoId} transform:^id(NSDictionary *response) { NSString *total = [response valueForKeyPath:@"photo.total"]; return total; }]; RACSignal *comments = [self signalFromAPIMethod:@"flickr.photos.getInfo" arguments:@{@"photo_id": photoId} transform:^id(NSDictionary *response) { NSString *total = [response valueForKeyPath:@"photo.comments._text"]; return total; }]; return [RACSignal combineLatest:@[favorites, comments] reduce:^id(NSString *favs, NSString *coms){ RWTFlickrPhotoMetadata *meta = [RWTFlickrPhotoMetadata new]; meta.comments = [coms integerValue]; meta.favorites = [favs integerValue]; return meta; }]; } |
The above code makes use of the signalFromAPIMethod:arguments:transform:
to create signals from the underlying delegate-based ObjectiveFLickr API. The code above creates a pair of signals, one that obtains the number of favorites, and the other obtains the number of comments.
Once you create both signals, the combineLatest:reduce:
method generates a new signal that is a combination of both.
This method waits for a next event from each of the source signals. The reduce
block is invoked with their contents, and the result becomes the next event for the combined signal.
Nice and simple!
Before you celebrate the simplicity of this method, return to signalFromAPIMethod:arguments:transform:
and fix a little error I alluded to earlier. Did you spot it?
This method creates a new OFFlickrAPIRequest
instance for each API request. However, the results of each request return via the delegate object, and in this case it happens to be self
.
As a result, in the case of concurrent requests there is no way to tell which invocation of flickrAPIRequest:didCompleteWithResponse:
corresponds to which request.
Fortunately, the ObjectiveFlickr delegate method signature includes the corresponding request as its first argument, so this problem is quite easy to fix with a bit more ReactiveCocoa.
Within signalFromAPIMethod:arguments:transform:
, replace the pipeline that handles the successSignal
with the following:
@weakify(flickrRequest) [[[[successSignal filter:^BOOL(RACTuple *tuple) { @strongify(flickrRequest) return tuple.first == flickrRequest; }] map:^id(RACTuple *tuple) { return tuple.second; }] map:block] subscribeNext:^(id x) { [subscriber sendNext:x]; [subscriber sendCompleted]; }]; |
This simply adds a filter operation that removes any delegate method invocations that relate to requests other than the one that generates the current signal.
Note: The above code uses @weakify
to create a weak reference to flickrRequest
. This is because the flickrRequest
variable is in the same scope as the block, and mirrors the classic retain-self issue.
I recommend profiling this code with and without these macros to see the memory leak for yourself. This one is a pretty subtle and could easily make it into a final build!
The final step is to use this signal within the ViewModel layer.
Open RWTSearchResultsViewModel.m and import the following header:
#import "RWTFlickrPhoto.h" |
Further down the same file, add the following to the end of the initializer:
RWTFlickrPhoto *photo = results.photos.firstObject; RACSignal *metaDataSignal = [[services getFlickrSearchService] flickrImageMetadata:photo.identifier]; [metaDataSignal subscribeNext:^(id x) { NSLog(@"%@", x); }]; |
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. The table view binding helper only binds visible cells, so only the first few ViewModels will request their metadata.
Build, run and watch as the metadata now dynamically fetches as you scroll:
That’s pretty cool isn’t it?
Throttling
Unfortunately, there is still another problem to address — a developer’s job is never done. If you scroll really fast you can trick the application into fetching metadata for many images almost simultaneously.
Not only does this irritate your API, but it also hinders performance. Scrolling becomes choppy as these requests take a bigger and bigger chunk out of your processing power.
In order to solve this problem, the application should only initiate a metadata request when a photo is on display for a short period of time. Currently the isVisible
property of the ViewModel is set to YES, but never gets set back to NO. That’s the first thing to fix.
Open RWTSearchResultsTableViewCell.m and change the code you just added in bindViewModel:
to set the isVisible
property as follows:
photo.isVisible = YES; [self.rac_prepareForReuseSignal subscribeNext:^(id x) { photo.isVisible = NO; }]; |
The isVisible
property is set to YES
when the ViewModel binds to the View as before. But it reverts to NO
when the cell is removed from the table view and recycled.
You accomplish this via the rac_prepareForReuseSignal
signal that you added to UITableViewCell
by ReactiveCocoa.
Return to RWTSearchResultsItemViewModel
. This ViewModel needs to observe changes to the isVisible
property, and fire a request for metadata when this property is YES for more than one second.
ReactiveCocoa provides you an elegant way to achieve this.
Within RWTSearchResultsItemViewModel.m, update the initialize
method by , removing the creation of the fetchMetadata
signal. Then, replace it with the following:
// 1. RACSignal *visibleStateChanged = [RACObserve(self, isVisible) skip:1]; // 2. RACSignal *visibleSignal = [visibleStateChanged filter:^BOOL(NSNumber *value) { return [value boolValue]; }]; RACSignal *hiddenSignal = [visibleStateChanged filter:^BOOL(NSNumber *value) { return ![value boolValue]; }]; // 3. RACSignal *fetchMetadata = [[visibleSignal delay:1.0f] takeUntil:hiddenSignal]; |
Taking each step in turn:
- A signal is created by observing the
isVisible
property. The first next event fired by this signal will contain the initial state of this property. Since you’re only interested in changes to this property value, the skip operation suppresses the first event. - A pair of signals are created by filtering the
visibleStateChanged
signal, one indicating the transition from visible to hidden, and the other from hidden to visible. - Here’s the magic part! The
fetchMetadata
signal is created by delaying thevisibleSignal
by one second, providing a pause before fetching metadata. ThetakeUntil
operation ensures that if the cell becomes hidden again before the one second time interval, the next event from thevisibleSignal
is suppressed and metadata is not fetched.
Can you imagine how much more complex that would be without ReactiveCocoa?
Build, run and celebrate the fact that favorites and comments for an image only load after a short pause. As a result, you’ll enjoy silky-smooth scrolling. You could also add logging, via the logAll
operation, to observe the frequency of the API requests.
Error Handling
Currently the code that searches Flickr only handles the flickrAPIRequest:didCompleteWithResponse:
method defined on the OFFlickrAPIRequestDelegate
protocol.
Unfortunately, network requests can — and do — go wrong for a number of reasons. Any good application must handle such errors with elegance and keep the users informed so they don’t do something drastic out of frustration, like chuck their iPhones into the nearest body of water.
The delegate also defines a method, flickrAPIRequest:didFailWithError:
, which is invoked if something goes wrong with a request. In this section, you’re going to use this to handle errors and display an alert to the user.
RWTFlickrSearch
protocol defines your Model-layer interface for searching Flickr, and it returns results via signals.
You might recall from the earlier tutorials that signals emit next, completed and error events. As a result, there are no changes required to this interface to relay error conditions.
Open RWTFlickrSearchImpl.m and locate the signalFromAPIMethod:arguments:transform:
method for all the API requests.
Within this method, add the following just before the creation of the successSignal variable:
RACSignal *errorSignal = [self rac_signalForSelector:@selector(flickrAPIRequest:didFailWithError:) fromProtocol:@protocol(OFFlickrAPIRequestDelegate)]; [errorSignal subscribeNext:^(RACTuple *tuple) { [subscriber sendError:tuple.second]; }]; |
The above code creates a signal from the delegate method, subscribes to the signal, and sends an error if any error events occur.
The tuple passed to the subscribeNext
block contains the variables passed to the flickrAPIRequest:didFailWithError:
method. As a result, tuple.second
obtains the source error and uses it for the error event.
This is a nifty solution, don’t you think? Now all of the API requests have built-in error handling! The next step is to put this to use.
The RWTFlickrSearchViewModel
does not expose signals directly to the View, instead it exposes state (via properties) and a command. You need to expand the interface for this class to provide error reporting.
Open RWTFlickrSearchViewModel.h and add the following property:
@property (strong, nonatomic) RACSignal *connectionErrors; |
Open RWTFlickrSearchViewModel.m and add the following to the end of the initialize
method:
self.connectionErrors = self.executeSearch.errors; |
The executeSearch
property is a RACCommand
from the ReactiveCocoa framework. The RACCommand
class has an errors
property that forwards any errors that occur when the command is executes. Incidentally, RACCommand
is also a signal.
To handle these errors, open RWTFlickrSearchViewController.m and add the following to the bottom of the initWithViewModel:
method.
[_viewModel.connectionErrors subscribeNext:^(NSError *error) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Connection Error" message:@"There was a problem reaching Flickr." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; }]; |
This shows an alert view whenever an error occurs.
Build, run, disconnect your network and enjoy the new error handling support:
Are you wondering why the requests that fetch favorites and comments don’t report errors? It’s by design; those incomplete requests don’t significantly affect the application’s usability.
Note: For a fun challenge, you might want to try handling these errors.
Personally, I’d add error signals to the RWTSearchResultsItemViewModel
. Rather than handle these directly in the corresponding View, RWTSearchResultsTableViewCell
, I’d centralize the error handling by aggregating the errors within the parent RWTFlickrSearchViewModel
, allowing consistent error handling.
Adding a Recent Search list
You know your user is going to want to go back and ogle at some of the same pictures repeatedly. So, why not make it easy for him or her?
If you recall the images at the start of this article, the final application has a list of recent searches that display below the search text field:
Now you just need to add this functionality, and it’s a step I’m going to issue as a challenge!
Yes, that’s right. I’m leaving you to fend for yourself, but I have total confidence that you know enough to handle it. It’s time to put your MVVM skills to practice…
To start you off in the right direction, here’s a brief summary of how I would do it:
- I’d create a ViewModel that represents each of the previous searches, with properties that detail the search text, number of matches and the first matching image.
- I’d modify RWTFlickrSearchViewModel to expose an array of these new ViewModels as a property. Whenever a search executes successfully, this property would update with the new results.
- Rendering this array of ViewModels is quite simple with the
CETableViewBindingHelper
, and I’ve already added a suitable cell within the project,RWTRecentSearchItemTableViewCell
.
If you want to take it further, how about making it so that tapping a recent search repeats the search? This will involve exposing a command that executes when the user taps one of these recent search items.
If you got stuck at any point within this MVVM tutorial, you can find all the code on GitHub with a commit for each build-and-run step. It also contains a solution to this challenge, but please try it for yourself first!
Where To Go From Here?
Again, here is the final example project from this MVVM tutorial series (including the challenge results).
This two-part tutorial has covered quite a lot of ground, so I thought it might help to recap some of the key points in easily-digestible bullet points.
Regarding the MVVM pattern:
- MVVM is a recent variant on the MVC pattern that is gaining popularity.
- The MVVM pattern results in very ‘thin’ Views that improve code clarity and enhance testability.
- The visibility is strictly View => ViewModel => Model, with updates in the ViewModel reflecting in the View via bindings.
- The ViewModel should never hold a reference to the View.
- The ViewModel can be thought of as the model-of-the-view, it exposes properties that directly reflect the state of the View, together with commands that execute because of user interaction.
- The Model layer typically exposes services, in this case an API for querying Flickr. Although another good example is a service that provides persistence, why not try persisting the recent searches in this app via a service?
- A good litmus test for an MVVM application is that it can run without the UI.
- ReactiveCocoa provides a powerful mechanism for binding ViewModels to Views. However it’s also used extensively within the ViewModel and Model layers.
Next time you create an app, why not give MVVM a try?
I hope you enjoyed learning about MVVM! If you have any questions or thoughts, please share them in the comments below.
MVVM Tutorial with ReactiveCocoa: Part 2/2 is a post from: Ray Wenderlich
The post MVVM Tutorial with ReactiveCocoa: Part 2/2 appeared first on Ray Wenderlich.