原文
You might have seen this joke on Twitter a while back:
你也许在 twitter 上见过一个笑话:
“iOS Architecture, where MVC stands for Massive View Controller” via Colin Campbell
“iOS 架构中的 MVC 全称为 Massive View Controller”
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.
对于 iOS 开发者,这是个痛心的问题,但是我确信你们都曾经在开发中遇到过这个问题,臃肿的且无法维护的 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!
MVVM 教程介绍了一种不同的的程序结构 Model-View-ViewModel ,ReactiveCocoa 使这个种模式更简单,提供了 MVC 之外的另一种优秀的选择,并且使用 view controller 更有充,更简洁。
Throughout the course of this MVVM tutorial, you’re going to be building a simple Flickr search application, shown below:
FinishedApp
Note: This tutorial is written in Objective-C, not Swift. It also targets Xcode 5, not Xcode 6, and all screenshots are made with Xcode 5. For best results, use Xcode 5 when going through this tutorial.
However, if you are interested in checking out the Swift version of this sample project, check out my recent blog post where I did just that!
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.
如果你需要快速复习一下 ReactiveCocoa ,我将简单重述一些重点。
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.
ReactiveCocoa 的核心是信号,由 RACSignal 类表示。信号可以产生一个事件流,包含三种类型:next, completeed, error 。
Using this simple model, ReactiveCocoa can serve as a replacement for the delegate pattern, target-action pattern, key-value observing, and more.
ReactiveCocoa 使用简单的模型就可以替代 delegate, target-action, KVO 等机制。
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.
创建信号的 API 基本相同,因此容易阅读,但是 ReactiveCocoa 的强大在于你可在信号上应用更高阶的操作。这些操作让你可执行复杂的过滤,转换,信号可进行高度简洁的协调。
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…
在 MVVM 中, ReactiveCocoa 是一个特别的角色。它是邦定 ViewModel 与 View 之间的胶水。
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.
MVVM 模式众所周知是一种 UI 设计模式。它是众多 MV* 模式中的一员,其中当然也包含 MVC,MVP 等等。
Each of these patterns is concerned with separating UI logic from business logic in order to make applications easier to develop and test.
每一种模式都致力于将 UI 逻辑与业务逻辑分离,使用应用更易开发与测试。
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.
回顾过去,可以帮助我们更好的理解 MVVM 模式。
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:
MVC 是最早的 UI 设计模式,可追溯到 70 年代的 Smalltalk 。
MVCPattern-2
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.
这种模式将 UI 分成 model 表示应用状态,view 负责组合 UI 控件,controller 处理交互,更新 model 。
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.
MVC 模式的最大问题是太模糊。定义看起来很美,但是是实现时,往往在 model, view, controller 之间产生循环的关系,并且变得异常庞大和糟糕。
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.
最近 Martin Fowler 介绍的 MVC 变种 Presentation Model ,其被 Microsoft 广泛使用,并称做 MVVM 。
MVVMPattern
At the core of this pattern is the ViewModel, which is a special type of model that represents the UI state of the application.
MVVM 模式的核心是 ViewModel ,专用来表示应用的 UI 的状态的 model 。
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.
它包含了每一个 UI 控件的详细状态。例如,text field 当前的 text, 某个 button 是否禁用。同时也表示了 view 可执行的动作。
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.
Note: Testing views is notoriously difficult because tests run as small, contained chunks of code. Usually, controllers add and configure views to the scene that rely on other application state. This means that meaning running small tests can become a fragile and cumbersome proposition.
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:
Then add the following initializer:
@interface RWTFlickrSearchViewController UIViewController
instancetypeinitWithViewModelRWTFlickrSearchViewModel viewModel;
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:
instancetypeinitWithViewModelRWTFlickrSearchViewModel viewModel
self super init;
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:
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:
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 initWithViewModelself.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!
ViewWithState
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:
bindViewModel
self.title self.viewModel.title;
RACself.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:
@interface RWTFlickrSearchImpl NSObject
Open RWTFlickrSearchImpl.m and provide the following implementation:
@implementation RWTFlickrSearchImpl
RACSignal flickrSearchSignalNSString searchString
return RACSignal empty
logAll
delay
logAll;
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;
@protocol RWTViewModelServices
id getFlickrSearchService;
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:
And update the initializer to take this as an argument:
instancetype initWithServicesidservices;
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 services;
Further down the same file, update the initializer:
instancetype initWithServicesidservices
self super init;
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
flickrSearchSignalself.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;
@interface RWTViewModelServicesImpl NSObject
Open RWTViewModelServicesImpl.m and implement as follows:
@interface RWTViewModelServicesImpl
@property strong, nonatomic RWTFlickrSearchImpl searchService;
@implementation RWTViewModelServicesImpl
instancetypeinit
self super init
_searchService RWTFlickrSearchImpl new;
return self;
idgetFlickrSearchService
return self.searchService;
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:
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
initWithServicesself.viewModelServices;
return RWTFlickrSearchViewController alloc
initWithViewModelself.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;
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;
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: