You might have seen this joke on Twitter a while back:
“iOS Architecture, where MVC stands for Massive View Controller” via Colin Campbell
This is a light-hearted ‘jab’ at iOS developers, but I am sure you have all seen this problem in practice; the overweight and unmanageable view controller.
This MVVM tutorial takes a look at a different pattern for structuring applications, Model-View-ViewModel, or MVVM. This pattern, facilitated by ReactiveCocoa, provides an excellent alternative to MVC, and guarantees sleek and lightweight view controllers!
Throughout the course of this MVVM tutorial, you’re going to be building a simple Flickr search application, shown below:
Before you start writing code, it’s time for a bit of theory!
A Brief Recap of ReactiveCocoa
This tutorial is primarily about MVVM, and assumes you have a working knowledge of ReactiveCocoa. If you haven’t used ReactiveCocoa before, I strongly advise following my earlier tutorial series that will introduce you to the topic.
If you’re in need of a quick refresher of ReactiveCocoa, I’ll briefly recap the salient points.
At the very core of ReactiveCocoa are signals, represented by the RACSignal class. Signals emit a stream of events, which are all one of three types, next, completed and error.
Using this simple model, ReactiveCocoa can serve as a replacement for the delegate pattern, target-action pattern, key-value observing, and more.
The signal API creates code that is more homogenous, and hence easier to read, but the real power of ReactiveCocoa is the higher-level operations you can apply to these signals. These operations allow you to perform complex filtering, transformation and signal coordination in a highly concise fashion.
Within the context of MVVM, ReactiveCocoa performs a very specific role. It provides the ‘glue’ that binds the ViewModel to the View. But you’re getting a little ahead of yourself now…
An Introduction to the MVVM pattern
The Model-View-ViewModel, or MVVM pattern as it’s commonly known, is a UI design pattern. It’s a member of a larger family of patterns collectively known as MV*, these include Model View Controller (MVC), Model View Presenter (MVP) and a number of others.
Each of these patterns is concerned with separating UI logic from business logic in order to make applications easier to develop and test.
Note: For a detailed look at design patterns in general, I recommend Eli’s or Ash Furrow’s articles.
To understand the MVVM pattern better, it helps to look back at its origins.
MVC was the first UI design pattern, and its origins track back to the Smalltalk language of the 1970s. The image below illustrates the main components of the MVC pattern:
This pattern separates the UI into the Model that represents the application state, the View, which in turn is composed of UI controls, and a Controller that handles user interactions and updates the model accordingly.
One of the big problems with the MVC pattern is that it’s quite confusing. The concepts look good, but often when people come to implement MVC, the seemingly circular relationships illustrated above result in the Model, View and Controller. In turn, they conflate into a big, horrible mess.
More recently Martin Fowler introduced a variation of the MVC pattern termed the Presentation Model, which was adopted and popularized by Microsoft under the name MVVM.
At the core of this pattern is the ViewModel, which is a special type of model that represents the UI state of the application.
It contains properties that detail the state of each and every UI control. For example, the current text for a text field, or whether a specific button is enabled. It also exposes the actions the view can perform, like button taps or gestures.
It can help to think of the ViewModel as the model-of-the-view.
The relationships between the three components of the MVVM pattern are simpler than the MVC equivalents, following these strict rules:
- The View has a reference to the ViewModel, but not vice-versa.
- The ViewModel has a reference to the Model, but not vice-versa.
If you break either of these rules, you’re doing MVVM wrong!
A couple of immediate advantages of this pattern are as follows:
- Lightweight Views – All your UI logic resides within the ViewModel, resulting in a very lightweight view.
- Testing – you should be able to run your entire application without the View, greatly enhancing its testability.
At this point, you might have spotted a problem. If the View has a reference to the ViewModel but not vice-versa, how does the ViewModel update the View?
Ah-ha!!! This is where the secret-sauce of the MVVM pattern comes in.
MVVM and Data Binding
The MVVM pattern relies on data-binding, a framework level feature that automatically connects object properties to UI controls.
As an example, within Microsoft’s WPF framework, the following markup binds the Text property of a TextField
to the Username
property on the ViewModel:
<TextField Text=”{DataBinding Path=Username, Mode=TwoWay}”/> |
The WPF framework ‘binds’ these two properties together.
The TwoWay
binding ensures that changes to the ViewModel’s Username
property propagate to the Text
property of the TextField
, and vice-versa, i.e. user-input is reflected in the ViewModel.
As another example, with the popular web-based MVVM framework Knockout, you can see the same the same binding feature in action:
<input data-bind=”value: username”/> |
The above binds a property of an HTML element with a JavaScript model.
Unfortunately, iOS lacks a data-binding framework, but this is where ReactiveCocoa acts as the ‘glue’ that connects the ViewModel together.
Looking at the MVVM pattern specifically from the perspective of iOS development, the ViewController and its associated UI — whether that is a nib, storyboard or constructed though code — composes the View:
…with ReactiveCocoa binding the two together.
Note: For a more detailed historical analysis of UI patterns I would highly recommend Martin Fowler’s GUI Architectures article.
Have you had enough theory? Well, if not feel free to go back and read it again. Oh, you’re good? Okay, then it’s time to have a go at creating your own ViewModels.
Starter Project Structure
First download the following starter project:
It uses CocoaPods to manage dependencies (if you’re new to CocoaPods, we have a tutorial for you!). Run pod install
to obtain the dependencies, verifying that you see the following output:
$ pod install Analyzing dependencies Downloading dependencies Installing LinqToObjectiveC (2.0.0) Installing ReactiveCocoa (2.1.8) Installing SDWebImage (3.6) Installing objectiveflickr (2.0.4) Generating Pods project Integrating client project |
You’ll learn more about each dependency as it’s put to use.
The starter project for this tutorial already contains the views for your application, implemented using view controllers and nib files. Open up the RWTFlickrSearch.xcworkspace generated by CocoaPods, build and run the starter project and you’ll see one of these views:
Take a little time to familiarize yourself with the project structure:
The Model and ViewModel groups are empty; you’ll be adding files to these shortly. The View group contains the following:
RWTFlickSearchViewController
: The main screen of the application, which contains a search text field and the ‘Go’ button.RWTRecentSearchItemTableViewCell
: A table cell that renders recent search results within the main screen.RWTSearchResultsViewController
: The search results screen that shows a table of images from Flickr.RWTSearchResultsTableViewCell
: A table cell that renders individual images from Flickr.
Time to add your first view model!
Your First ViewModel
Add a new class to the ViewModel group, naming it RWTFlickrSearchViewModel
and making it a subclass of NSObject
.
Open the newly added header file, and add a couple of properties:
@interface RWTFlickrSearchViewModel : NSObject @property (strong, nonatomic) NSString *searchText; @property (strong, nonatomic) NSString *title; @end |
The searchText
property represents the text displayed in the text field, and the title property represents the title displayed in the navigation bar.
Note: In order to make it easier to understand the application structure, the Views and ViewModels share a common name with a different suffix. For example RWTFlickrSearch-ViewModel
and RWTFlickrSearch-ViewController
.
Open RWTFlickrSearchViewModel.m and add the following:
@implementation RWTFlickrSearchViewModel - (instancetype)init { self = [super init]; if (self) { [self initialize]; } return self; } - (void)initialize { self.searchText = @"search text"; self.title = @"Flickr Search"; } @end |
This simply sets the initial state of this ViewModel.
The next step is to connect the ViewModel to the View. Remember that the View holds a reference to the ViewModel. In this case, it makes sense to add an initializer that constructs the View, given the respective ViewModel.
Open RWTFlickrSearchViewController.h and import the ViewModel header:
#import "RWTFlickrSearchViewModel.h" |
Then add the following initializer:
@interface RWTFlickrSearchViewController : UIViewController - (instancetype)initWithViewModel:(RWTFlickrSearchViewModel *)viewModel; @end |
Within RWTFlickrSearchViewController.m, add the following private property to the same class extension that holds the UI outlets:
@property (weak, nonatomic) RWTFlickrSearchViewModel *viewModel; |
Then add an init method as follows:
- (instancetype)initWithViewModel:(RWTFlickrSearchViewModel *)viewModel { self = [super init]; if (self ) { _viewModel = viewModel; } return self; } |
This stores a reference to the ViewModel that backs this View
Note: This is a weak reference; the View references the ViewModel, but doesn’t own it.
Add the following to the end of viewDidLoad
:
[self bindViewModel]; |
Then implement this method as follows:
- (void)bindViewModel { self.title = self.viewModel.title; self.searchTextField.text = self.viewModel.searchText; } |
The above code executes when the UI initializes and applies the ViewModel state to the View.
The final step is to create an instance of the ViewModel, and then supply it the View.
Within RWTAppDelegate.m, add the following import:
#import "RWTFlickrSearchViewModel.h" |
And a private property (within the class extension at the top of the file):
@property (strong, nonatomic) RWTFlickrSearchViewModel *viewModel; |
You’ll find that this class already has a createInitialViewController
method, update its implementation as follows:
- (UIViewController *)createInitialViewController { self.viewModel = [RWTFlickrSearchViewModel new]; return [[RWTFlickrSearchViewController alloc] initWithViewModel:self.viewModel]; } |
This creates a new instance of the ViewModel, and then it constructs and returns the View. This serves as the initial view for the application’s navigation controller.
Build and run to see that the View now has some state!
Congratulations, this is your first ViewModel. I’m going to have to ask you to contain your excitement! There’s still much to learn here ;]
You might have noticed you haven’t used any ReactiveCocoa yet. In its present form, any text the user enters into the search text field will not reflect in the ViewModel.
Detecting Valid Search State
In this section, you’re going to use ReactiveCocoa to bind the ViewModel and View together in order to connect both the search text field and button to the ViewModel.
Within RWTFlickrSearchViewController.m, update bindViewModel
as follows:
- (void)bindViewModel { self.title = self.viewModel.title; RAC(self.viewModel, searchText) = self.searchTextField.rac_textSignal; } |
You’re adding the rac_textSignal
property to the UITextField
class by using a category within ReactiveCocoa. It’s a signal that emits a next event containing the current text each time the text field updates.
The RAC macro is a binding; the above code updates the searchText
property of the viewModel
with the contents of each next event emitted by the rac_textSignal
.
In short, it ensures that the searchText
property always reflects the current UI state. If the above sounds like utter gibberish, you really need to re-visit the first two ReactiveCocoa tutorials!
The search button should only be enabled if the text the user has entered is valid. We’ll keep things simple and enforce the rule that they must enter more than three characters before they can execute a search.
Within RWTFlickrSearchViewModel.m add the following import:
#import <ReactiveCocoa/ReactiveCocoa.h> |
And update the initialize method as follows:
- (void)initialize { self.title = @"Flickr Search"; RACSignal *validSearchSignal = [[RACObserve(self, searchText) map:^id(NSString *text) { return @(text.length > 3); }] distinctUntilChanged]; [validSearchSignal subscribeNext:^(id x) { NSLog(@"search text is valid %@", x); }]; } |
Build, run and have a go at entering some text into the text field. You’ll now see a log message each time the text transitions between a valid or invalid state:
2014-05-27 18:03:26.299 RWTFlickrSearch[13392:70b] search text is valid 0 2014-05-27 18:03:28.379 RWTFlickrSearch[13392:70b] search text is valid 1 2014-05-27 18:03:29.811 RWTFlickrSearch[13392:70b] search text is valid 0 |
The above code uses the RACObserve
macro to create a signal from the ViewModel’s searchText
property (this is a ReactiveCocoa wrapper over KVO). A map operation converts the text into a stream of true and false values.
Finally, distinctUntilChanges
is used to ensure this signal only emits values when the state changes.
Note: If you’re having difficulty following, try breaking it down into smaller pieces. First add a RACObserve
, logging the output, then add the map followed by the distinctUntilChanged
operation as separate steps.
What you have seen so far is that ReactiveCocoa is used to bind the View to the ViewModel, ensuring that the two remain synchronized. Furthermore, ReactiveCocoa is used internally within the ViewModel to observe its own state and perform other operations.
This is a pattern you’ll see emerging throughout this MVVM tutorial. ReactiveCocoa is vital for binding the View and ViewModel together, but it’s also very useful within the other layers of your applications.
Adding a search command
In this section you’ll do something more useful with the validSearchSignal
: use it to create a command that is bound to the View.
Open RWTFlickrSearchViewModel.h and add the following import:
#import <ReactiveCocoa/ReactiveCocoa.h> |
And the following property:
@property (strong, nonatomic) RACCommand *executeSearch; |
RACCommand
is a ReactiveCocoa concept that represents a UI action. It comprises a signal, which is the result of the UI action, and the current state, which indicates whether the action is currently being executed.
Within RWTFlickrSearchViewModel.m add the following to the end of the initialize
method:
self.executeSearch = [[RACCommand alloc] initWithEnabled:validSearchSignal signalBlock:^RACSignal *(id input) { return [self executeSearchSignal]; }]; |
This creates a command that is enabled when the validSearchSignal
emits true.
Further down the same file, add the following method that provides the execution signal for the command:
- (RACSignal *)executeSearchSignal { return [[[[RACSignal empty] logAll] delay:2.0] logAll]; } |
Within this method, you’ll perform some business logic as a result of the command executing, and will return the result asynchronously via the signal.
For now, the above is merely a dummy implementation; the empty signal completes immediately. The delay operation adds a two-second delay to any next and completed events it receives. This is a very cunning way of making this code more realistic!
Note: The above signal pipeline has a couple of additional logAll
operations, and these are side effects that log all events that pass through them.
While they are rather helpful for debugging ReactiveCocoa code, feel free to remove these logging operations if you don’t find them to be necessary.
The final step is to wire this command to the View. Open RWTFlickrSearchViewController.m and add the following to the end of the bindViewModel
method:
self.searchButton.rac_command = self.viewModel.executeSearch; |
The rac_command
property is a ReactiveCocoa addition to UIButton
. The above code ensures that button taps result in the given command executing, and that the enabled state of the button reflects the enabled state of the command.
Build and run, enter some text and hit Go:
You’ll see that the button is only enabled when there are more than three characters in the text field. Furthermore, when you tap the button it’s disabled for two seconds, and becomes re-enabled when the execution signal completes.
From the console, you can also see the empty signal completing immediately, with the delay operation emitting this event two seconds later:
09:31:25.728 RWTFlickrSearch ... name: +empty completed 09:31:27.730 RWTFlickrSearch ... name: [+empty] -delay: 2.0 completed |
How cool is that?
Bindings, Bindings and More Bindings
The RACCommand
takes care of updating the state of the search button, but it’s on you to handle the visibility of the activity indicator.
RACCommand
exposes an executing property, and that’s a signal that emits true and false events to indicate when the command starts and ends execution. You can use these to reflect the current command state elsewhere in your application.
Within RWTFlickrSearchViewController.m, add the following to the end of bindViewModel
:
RAC([UIApplication sharedApplication], networkActivityIndicatorVisible) = self.viewModel.executeSearch.executing; |
This binds the networkActivityIndicatorVisible
property of UIApplication
to the command’s executing signal. This ensures that whenever the command executes, the small network activity indicator in the status bar will show itself.
Next, add the following:
RAC(self.loadingIndicator, hidden) = [self.viewModel.executeSearch.executing not]; |
When the command executes, the loading indicator should be hidden; this is the opposite of the property you just bound.
Fortunately, ReactiveCocoa has this covered with a not operation that can invert signals.
Finally, add the following:
[self.viewModel.executeSearch.executionSignals subscribeNext:^(id x) { [self.searchTextField resignFirstResponder]; }]; |
The above ensures that the keyboard hides whenever the command executes. The executionSignals
property emits the signals that generate each time the command executes.
This property is a signal of signals (as introduced in the previous tutorial). Whenever a new command execution signal is created and emitted, the keyboard is hidden.
Build and run the application to see the above code in action.
Where’s the Model?
At this point you have a clearly identifiable View (RWTFlickrSearchViewController
) and ViewModel (RWTFlickrSearchViewModel
), but, ummm, where is the Model?
The answer is simple: there isn’t one!
The current app executes a command in response to the user tapping the search button, but the implementation does nothing ‘ of value.
What needs to happen is for the ViewModel to search Flickr using the current searchText, and return a list of matching pictures.
You could add this logic directly into the ViewModel, but trust me, you would regret it! If this was a view controller, I would be willing to bet you would do exactly that.
The ViewModel exposes properties that represent the UI state, it also exposes commands — and often methods — that represent UI actions. It’s responsible for managing changes to the UI state based on user interactions.
However, it’s not responsible for the actual business logic that executes because of these interactions. That is the job of the Model.
In this next step, you’re going to add a Model layer to the application. There’s a fair bit of ‘scaffolding code’ required, so just work through and you’ll get to something more interesting shortly.
Within the Model group, add a new protocol named RWTFlickrSearch
and provide the following implementation:
#import <ReactiveCocoa/ReactiveCocoa.h> @import Foundation; @protocol RWTFlickrSearch <NSObject> - (RACSignal *)flickrSearchSignal:(NSString *)searchString; @end |
This protocol defines the initial interface for the Model layer, and moves the responsibility of searching Flickr out of the ViewModel.
Next add a new NSObject
subclass, RWTFlickrSearchImpl
to the same group, and then update the interface to adopt this newly added protocol:
@import Foundation; #import "RWTFlickrSearch.h" @interface RWTFlickrSearchImpl : NSObject <RWTFlickrSearch> @end |
Open RWTFlickrSearchImpl.m and provide the following implementation:
@implementation RWTFlickrSearchImpl - (RACSignal *)flickrSearchSignal:(NSString *)searchString { return [[[[RACSignal empty] logAll] delay:2.0] logAll]; } @end |
Are you feeling like this might be dejavu? If so, that’s because this is the same ‘dummy’ implementation that was located within the ViewModel previously.
The next step is to make use of the Model layer from within the ViewModel. In the ViewModel group add a new protocol named RWTViewModelServices
and update it as follows:
@import Foundation; #import "RWTFlickrSearch.h" @protocol RWTViewModelServices <NSObject> - (id<RWTFlickrSearch>) getFlickrSearchService; @end |
This protocol defines a single method that allows the ViewModel to obtain a reference to an implementation of the RWTFlickrSearch protocol.
Open RWTFlickrSearchViewModel.h and import this new protocol:
#import "RWTViewModelServices.h" |
And update the initializer to take this as an argument:
- (instancetype) initWithServices:(id<RWTViewModelServices>)services; |
Within RWTFlickrSearchViewModel.m
, add a class extension and a private property to hold a reference to the view model services:
@interface RWTFlickrSearchViewModel () @property (nonatomic, weak) id<RWTViewModelServices> services; @end |
Further down the same file, update the initializer:
- (instancetype) initWithServices:(id<RWTViewModelServices>)services { self = [super init]; if (self) { _services = services; [self initialize]; } return self; } |
This simply stores a reference to the services.
Finally, update the executeSearchSignal
method as follows.
- (RACSignal *)executeSearchSignal { return [[self.services getFlickrSearchService] flickrSearchSignal:self.searchText]; } |
The above method now delegates to the model to perform the search.
The final step is to connect the Model and ViewModel.
Within the ‘root’ group of the project RWTFlickrSearch, add a new NSObject
subclass named RWTViewModelServicesImpl
. Open RWTViewModelServicesImpl.h and adopt the RWTViewModelServices
protocol:
@import Foundation; #import "RWTViewModelServices.h" @interface RWTViewModelServicesImpl : NSObject <RWTViewModelServices> @end |
Open RWTViewModelServicesImpl.m and implement as follows:
#import "RWTViewModelServicesImpl.h" #import "RWTFlickrSearchImpl.h" @interface RWTViewModelServicesImpl () @property (strong, nonatomic) RWTFlickrSearchImpl *searchService; @end @implementation RWTViewModelServicesImpl - (instancetype)init { if (self = [super init]) { _searchService = [RWTFlickrSearchImpl new]; } return self; } - (id<RWTFlickrSearch>)getFlickrSearchService { return self.searchService; } @end |
This class simply creates an instance of RWTFlickrSearchImpl
, the Model layer service for searching Flickr, and provides it to the ViewModel upon request.
Finally, open RWTAppDelegate.m and add the following import:
#import "RWTViewModelServicesImpl.h" |
And a new private property:
@property (strong, nonatomic) RWTViewModelServicesImpl *viewModelServices; |
And update the createInitialViewController
method as follows:
- (UIViewController *)createInitialViewController { self.viewModelServices = [RWTViewModelServicesImpl new]; self.viewModel = [[RWTFlickrSearchViewModel alloc] initWithServices:self.viewModelServices]; return [[RWTFlickrSearchViewController alloc] initWithViewModel:self.viewModel]; } |
Build and run, and verify the application works in exactly the same way it did previously!
No, this was not the most exciting change to make, but take a moment to look at the ‘shape’ of this new code.
The Model layer exposes a ‘service’ that the ViewModel consumes. A protocol defines this service interface, providing loose coupling.
You could use this approach to provide a dummy service implementation for unit tests. The application now has the correct Model-View-ViewModel structure. To briefly recap:
- The Model layer exposes services and is responsible for providing business logic for the application. In this case, it provides a service to search Flickr.
- The ViewModel layer represents the view-state of the application. It also responds to user interactions and ‘events’ that come from the Model layer, each of which are reflected by changes in view-state.
- The View layer is very thin and simply provides a visualisation of the ViewModel state and forwards user interactions.
Note: In this application the Model layer exposes its services using ReactiveCocoa signals. This framework is useful for far more than just bindings!
Searching Flickr
In this section you’re going to provide a real Flickr search implementation, yes, things are about to become exciting ;]
The first step is to create the model objects that represent the search results.
Within the Model group, add a new NSObject
subclass called RWTFlickrPhoto
, adding three properties to the interface as follows:
@interface RWTFlickrPhoto : NSObject @property (strong, nonatomic) NSString *title; @property (strong, nonatomic) NSURL *url; @property (strong, nonatomic) NSString *identifier; @end |
This is a model object that represents a single photo returned by Flickr’s search APIs.
Open RWTFlickrPhoto.m and add an implementation for the describe method:
- (NSString *)description { return self.title; } |
This allows you to test the search implementation by logging the results before you move on to the required UI changes.
Next, add another model object RWTFlickrSearchResults
that is also an NSObject
subclass, adding the following properties to the interface:
@import Foundation; @interface RWTFlickrSearchResults : NSObject @property (strong, nonatomic) NSString *searchString; @property (strong, nonatomic) NSArray *photos; @property (nonatomic) NSUInteger totalResults; @end |
This represents a collection of photos as returned by a Flickr search.
Open RWTFlickrSearchResults.m and add the following implementation for the description
method (again for logging purposes):
- (NSString *)description { return [NSString stringWithFormat:@"searchString=%@, totalresults=%lU, photos=%@", self.searchString, self.totalResults, self.photos]; } |
It’s time to write the code that searches Flickr!
Open RWTFlickrSearchImpl.m and add the following imports:
#import "RWTFlickrSearchResults.h" #import "RWTFlickrPhoto.h" #import <objectiveflickr/ObjectiveFlickr.h> #import <LinqToObjectiveC/NSArray+LinqExtensions.h> |
This imports the model objects you just created, plus a couple of external dependencies that CocoaPods loaded:
- ObjectiveFlickr – this is an Objective-C API that wraps the Flickr API. It handles authentication and parses the API responses. It’s much easier to use this library than Flickr’s APIs directly.
- LinqToObjectiveC – this library provides a fluent and functional interface for querying, filtering and transforming arrays and dictionaries.
Still within RWTFlickrSearchImpl.m add a class extension as follows:
@interface RWTFlickrSearchImpl () <OFFlickrAPIRequestDelegate> @property (strong, nonatomic) NSMutableSet *requests; @property (strong, nonatomic) OFFlickrAPIContext *flickrContext; @end |
This adopts the OFFlickrAPIRequestDelegate
protocol, from the ObjectiveFlickr library and adds a couple of private properties. You’ll see how these are used very soon.
Further down the same file add the following initializer:
- (instancetype)init { self = [super init]; if (self) { NSString *OFSampleAppAPIKey = @"YOUR_API_KEY_GOES_HERE"; NSString *OFSampleAppAPISharedSecret = @"YOUR_SECRET_GOES_HERE"; _flickrContext = [[OFFlickrAPIContext alloc] initWithAPIKey:OFSampleAppAPIKey sharedSecret:OFSampleAppAPISharedSecret]; _requests = [NSMutableSet new]; } return self; } |
This creates a Flickr ‘context’ to store the data ObjectiveFlickr requires to generate API requests.
Note: To use ObjectiveFlickr you’ll need to create a Flickr App Key via the Flickr App Garden. It’s free and only takes a few steps.
Be sure to request a non-commercial key.
The ObjectiveFlickr API is pretty typical. You create API requests and the resulting success or failure is reported via delegate methods, as defined by the OFFlickrAPIRequestDelegate
protocol mentioned earlier.
The current API exposed by your Model-layer service class, as defined by the RWTFlickrSearch
protocol, has a single method that searches for photos based on a text search string.
However, in the near future you’re going to be adding further methods.
Because of this, you’re going to dive straight in with a generic approach for adapting this delegate-based API to use signals. Hang on tight!
Still within RWTFlickrSearchImpl.m add the following method:
- (RACSignal *)signalFromAPIMethod:(NSString *)method arguments:(NSDictionary *)args transform:(id (^)(NSDictionary *response))block { // 1. Create a signal for this request return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { // 2. Create a Flick request object OFFlickrAPIRequest *flickrRequest = [[OFFlickrAPIRequest alloc] initWithAPIContext:self.flickrContext]; flickrRequest.delegate = self; [self.requests addObject:flickrRequest]; // 3. Create a signal from the delegate method RACSignal *successSignal = [self rac_signalForSelector:@selector(flickrAPIRequest:didCompleteWithResponse:) fromProtocol:@protocol(OFFlickrAPIRequestDelegate)]; // 4. Handle the response [[[successSignal map:^id(RACTuple *tuple) { return tuple.second; }] map:block] subscribeNext:^(id x) { [subscriber sendNext:x]; [subscriber sendCompleted]; }]; // 5. Make the request [flickrRequest callAPIMethodWithGET:method arguments:args]; // 6. When we are done, remove the reference to this request return [RACDisposable disposableWithBlock:^{ [self.requests removeObject:flickrRequest]; }]; }]; } |
This method takes an API request, as detailed by the method name and passed arguments, and then transforms the response using the given block argument. You’ll see how this works very soon.
Regarding the method itself, there’s quite a lot going on here. Consider each step, in turn:
- The
createSignal
method creates a new signal. The subscriber that passes to the signal block exposes methods that allow you to send next, error and completed events to the signals’ subscribers. - An ObjectiveFlickr request is constructed, and a reference to this request is stored in the requests set. Without this code, the
OFFlickrAPIRequest
would not be retained! - The
rac_signalForSelector:fromProtocol:
method creates a signal from the delegate method that indicates the completion of the Flickr API request. - The signal is subscribed to, and the result transforms and gets sent as the result of the signal that is being created (more on this later).
- The ObjectiveFlickr API request is invoked.
- When the signal is disposed, this block ensures that the reference to the Flickr request is removed, avoiding memory leaks.
Now let’s have a look at the pipeline from step (4) in more detail:
[[[successSignal // 1. Extract the second argument map:^id(RACTuple *tuple) { return tuple.second; }] // 2. transform the results map:block] subscribeNext:^(id x) { // 3. send the results to the subscribers [subscriber sendNext:x]; [subscriber sendCompleted]; }]; |
rac_signalForSelector:fromProtocol:
method creates the successSignal
, and it also creates signals from delegate method invocations.
Each time the delegate method is invoked, a next event emits with a RACTuple
that contains the method arguments. The pipeline above performs the following steps:
- A map operation extracts the second argument from the
flickrAPIRequest:didCompleteWithResponse:
delegate method, theNSDictionary
response. - The block that passes as an argument to this method transforms the result. You’ll shortly see how this converts from the dictionary to the model object representation.
- Finally, the transformed response is sent as a next event, and the signal completes.
Note: There is a subtle issue with this code, think about what might happen if there are concurrent requests. You’ll fix that in the second part of this MVVM tutorial, but if you’re feeling adventurous, why not try to tackle it now?
The final step is to implement the Flickr search method as follows:
- (RACSignal *)flickrSearchSignal:(NSString *)searchString { return [self signalFromAPIMethod:@"flickr.photos.search" arguments:@{@"text": searchString, @"sort": @"interestingness-desc"} transform:^id(NSDictionary *response) { RWTFlickrSearchResults *results = [RWTFlickrSearchResults new]; results.searchString = searchString; results.totalResults = [[response valueForKeyPath:@"photos.total"] integerValue]; NSArray *photos = [response valueForKeyPath:@"photos.photo"]; results.photos = [photos linq_select:^id(NSDictionary *jsonPhoto) { RWTFlickrPhoto *photo = [RWTFlickrPhoto new]; photo.title = [jsonPhoto objectForKey:@"title"]; photo.identifier = [jsonPhoto objectForKey:@"id"]; photo.url = [self.flickrContext photoSourceURLFromDictionary:jsonPhoto size:OFFlickrSmallSize]; return photo; }]; return results; }]; } |
The above method uses the signalFromAPIMethod:arguments:transform:
method that you added in the previous step. The flickr.photos.search
API method searches for photos, with the search criteria supplied as a dictionary.
The block passed to the transform argument simply converts the NSDictionary
response into an equivalent model-object representation, making it easier to use within the ViewModel.
This code makes use of the linq_select
method that is added to NSArray
via LinqToObjectiveC, providing a functional API for array transformation.
Note: For more complex JSON-to-object mapping I’d recommend having a look at Mantle, although this particular model would require a Mantle 2.0 feature in order to map it correctly!
The final step is to open RWTFlickrSearchViewModel.m, and then update the search signal to log its results:
- (RACSignal *)executeSearchSignal { return [[[self.services getFlickrSearchService] flickrSearchSignal:self.searchText] logAll]; } |
Build, run and enter a search string to see the result of this signal logging to the console:
2014-06-03 [...] <RACDynamicSignal: 0x8c368a0> name: +createSignal: next: searchString=wibble, totalresults=1973, photos=( "Wibble, wobble, wibble, wobble", "unoa-army", "Day 277: Cheers to the freakin' weekend!", [...] "Angry sky", Nemesis ) |
Note: If you don’t get results, double check your Flickr API key and shared secret.
This brings part one of this MVVM tutorial to a close, but before finishing off there is a very important aspect of the current application code that you’ve not fully explored…
Memory Management
If you recall from the earlier tutorials, I mentioned you have to use the block-based ReactiveCocoa API with some caution in order to avoid creating retain-cycles, which ultimately led to memory leaks.
If a block uses ‘self’, and ‘self’ has a strong reference to the block a retain cycle forms. In the previous tutorial you saw how to use the @weakify
and @strongify
macros in order to break these retain cycles.
Are you wondering why the implementation of the signalFromAPIMethod:arguments:transform:
doesn’t use these macros when referencing self
?
It’s because the block serves as an argument to the createSignal:
method, which doesn’t form a strong reference between self
and the block.
Confused? Well, you don’t have to take my word for it, just test the code to find those memory leaks.
Run the application using the Product / Profile option, select the Allocations profile. When the application starts, use the filter in the top right corner to search for classes that contain the text ‘OFF’, i.e. those from the ObjectiveFlickr framework.
You’ll find that on start-up it creates a single instance of OFFlickrAPIContext
. When you search, you’ll see that an instance of OFFlickrAPIRequest
is created and exists for the lifetime of the query:
And the good news is that the OFFlickrAPIRequest
instance is de-allocated when the API request completes. Now you have conclusive proof that the block that initiates the Flickr request is not retained!
I would encourage you to repeat this profiling process periodically to ensure that the heap only contains the objects you expect.
Where To Go From Here?
Here is an example project with the code from this tutorial so far.
This concludes the first part of this MVVM Tutorial with ReactiveCocoa!
In the next part, you’ll look at how to initiate a view controller transition from the ViewModel and implement further Flickr API requests in order to make this application more feature rich.
In the meantime, if you have any questions or comments please join the forum discussion below!
MVVM Tutorial with ReactiveCocoa: Part 1/2 is a post from: Ray Wenderlich
The post MVVM Tutorial with ReactiveCocoa: Part 1/2 appeared first on Ray Wenderlich.