Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4399

MVVM Tutorial with ReactiveCocoa: Part 2/2

$
0
0

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 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:

WithMetadata

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:

  1. 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.
  2. 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.
  3. Here’s the magic part! The fetchMetadata signal is created by delaying the visibleSignal by one second, providing a pause before fetching metadata. The takeUntil operation ensures that if the cell becomes hidden again before the one second time interval, the next event from the visibleSignal 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:

ErrorMessages

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:

FinishedApp

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:

  1. 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.
  2. 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.
  3. 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.


Viewing all articles
Browse latest Browse all 4399

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>