此文是翻译作品,原文见:http://www.raywenderlich.com/74106/mvvm-tutorial-with-reactivecocoa-part-1
此文是我学习过程中遇到的很好的文章,因为搜不到翻译版本,因此自己翻译了,希望能帮到大家。同时翻译的时候我也好好精进了一下我的markdown语法
你可能在Twitter上听过这样的笑话:
“iOS框架,大量View Controller的产生地” by Colin Campbell。
这在iOS开发者心中是个轻松地“戳”,但是我确信你已经在练习中遇到过这些问题了——臃肿的,难以管理的View Controller。
这个MVVM的开发教程用一个不同的模式来构建一个app,Model-View-View-ViewModel,或者简称MVVM,这个模式因为ReactiveCocoa的诞生更加方便,带来了一个完美的MVC模式的替换模式,和一个轻便的,易于管理的View Controller!
通过这个MVVM教程,你要去建立一个简单的搜索app叫做Flicker search,像下面的图片一样:
注意:这个教程是使用Objective-C开发的,如果你要看我用swift开发的教程,点击这里,在我的博客里面可以看到。
在你开始写代码前,是时候讲一些理论知识了!
一个对ReactiveCocoa的简单介绍
这个教程主要是关于MVVM的,并且假设你对ReactiveCocoa有一定的了解,如果你没有用过ReactiveCocoa,我强烈建议你看我早一些的教程,这个教程会教给你很多。
ReactiveCocoa最核心的东西无疑是 signals,在RACSignal 这个类里面。signals给事件发出一个流,这个流(stream)有三种类型: next、 completed 和 error。
运用这些简单的模式,ReactiveCocoa 可以用来替代代理模式(delegate pattern),观察者模式(KVO)和 target-action pattern,以及更多。
用signal的API编写出来的代码更加均匀,因此更加容易阅读,但是ReactiveCocoa真正的强大的地方在于是你对signals的高级操作,这些操作允许你进行复杂的过滤(filter),转化(transformation)以及用简单的方式协调(coordination)。
在MVVM的环境下,ReactiveCocoa扮演了极其重要的角色,它提供了强大的粘合力在View和ViewModel中间,这些对你还有一点点的超前。
MVVM开发模式的介绍
MVVM——Model-View-ViewModel,在通常的理解中是一个设计的模式,他是MV家族的一个成员,这个家族包括MVC、MVP等等。
每一个MV家族中的模式开始关心如何将UI和业务逻辑分开,因为这样更便利于开发和测试。
注意:如果想要深入了解开发设计模式,我推荐Eli’s和Ash Furrow’s的文章。
了解MVVM的起源有助于你更加了解这个模式。
MVC是第一个用户界面设计模式( UI design pattern),可以追溯到1970年代的Smalltalk language。下面这个图说明了MVC的主要运作模式:
这个模式将用户界面分为三种:
- Model,用来呈现应用状态。
- View,由视图控制器组成。
- Controller,处理用户交互并且更新model。
MVC的一个重大问题令人十分困扰,这个概念很好很完美,但是当经常人们开始实现MVC的时候,Model-View-Controller看似圆形的关系,反过来,他们合并成了一个可怕的巨大的麻烦。
不久之前Martin Fowler 向我们介绍了一个由MVC衍生出来的表现模式,并被微软接受并流行开来。
这个模式的核心是ViewModel,是一种特殊的Model,用来展示应用中UI的状态。
它包含了每一个UI控制器(Controller)的详细状态和属性,例如,一个TextFeild当前的文字,或者一个按钮的可否点击的状态,它也展现了当前视图的一系列动作,例如按钮点击或者手势操作。
将VIewModel理解成为View的Model(model of the view)可以更好地帮你去思考ViewModel。
MVVM遵循以下规则
- 1.View用来展现VIewModel,但是VIewModel不能展现View。
- 2.VIewModel用来展现Model,但是也不可反过来。
如果你打破了任何这个规则,你的MVVM就错了!
这种规则的优势如下:
- 1.更加轻量级的VIew层,所有业务逻辑都被移到ViewModel中。
- 2.更易于测试,你可以在没有View的情况下启动你的应用,大大提高了可测试性。
注意:测试视图是众所周知的困难,因为测试运行的小的包含的代码块。通常,控制器会在依赖于其他应用程序状态的场景中添加和配置视图。这意味着,意义上的小测试,可以成为一个脆弱而繁琐的命题。
因此,你可能会想提出一个问题,如果只是View可以展现VIewModel,而ViewModel不能反过来展现View的话,那么ViewModel如何更新View呢?啊哈!!这就是MVVM的秘诀了!
MVVM和数据绑定(Data Binding)
MVVM模式依赖于数据绑定,一个框架级的功能,自动连接对象属性的用户界面控件。
有一个例子,在微软的WPF框架,下面一个例子将TextField的文本和ViewModel的Username绑定。
WPF的框架将这两个成员变量“绑定”。
这个双向的绑定确保了ViewModel的Username的改变同时TextFeild的文本也改变,反之亦然,用户的输入也将改变ViewModel中的参数值。
另一个例子,基于web的流行的一个MVVM框架Knockout, 你可以发现两个框架中数据绑定的相同的特点。
上面的绑定将HTML的元素和JavaScript的模型绑定。
不幸的是,iOS缺少一个数据绑定框架,但是这就是ReactiveCocoa所充当的“胶水”作用。
具体从iOS开发的角度去看MVVM,ViewController和它相关的UI——不论是xib、storyboard或者是代码组成的视图(View):
ReactiveCocoa将两者绑定起来。
注意:对于UI的各种实现方式,我高度推荐Martin Fowler的GUI Architectures article。
你学到了足够的理论知识了吗?如果没有,请回头去再看一遍。当然,如果你学得够好了,那么现在是时候开始创造你自己的ViewModel了。
开始项目准备
首先下载这个开始工程
- FlickrSearchStarterProject.zip
这个项目使用CocoaPods去管理依赖库(如果你不知道CocoaPods,我们这里有个教程),运行pod install去安装依赖库,确认你看到了一下输出:
$ 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
你会学到每个依赖库是干吗的。
本教程的开始工程包含了一个View,通过xib实现,打开RWTFlickrSearch.xcworkspace,并且运行,然后你会看到以下页面:
花一点时间去熟悉这个项目结构:
Model和VIewModel的groups都是空的,你要为这两个group添加文件,项目已含有的文件是做这些的:
- RWTFlickSearchViewController:项目的主页面,包含了一个搜索框,和一个“GO”按钮。
- RWTRecentSearchItemTableViewCell:一个cell显示来自Flicker的第三方图片
是时候开始写你的第一个view model 了!
你的第一个ViewModel
在ViewModel这个group里添加一个新的类,将之命名为RWTFlickrSearchViewModel并且使他继承NSObject。
打开它并在头文件添加下面的声明:
@interface RWTFlickrSearchViewModel : NSObject
@property (strong, nonatomic) NSString *searchText;
@property (strong, nonatomic) NSString*title;
@end
searchText提供一个字符串显示在textfield上,成员变量title提供在navigation bar上显示的标题。
注意 :为了更容易的理解项目结构,View和ViewModel用了相同的名字和不同的后缀,例如:RWTFlickrSearch-ViewModel
和 RWTFlickrSearch-ViewController。
打开 RWTFlickrSearchViewModel.m 并且添加如下代码
@implementation RWTFlickrSearchViewModel
- (instancetype)init {
self = [super init];
if (self) {
[self initialize];
}
return self;
}
- (void)initialize {
self.searchText = @"search text";
self.title = @"Flickr Search";
}
@end
这段代码初始化了这个ViewModel。
下一步是讲如何将ViewModel和View关联到一起,记住View和ViewModel的关联,因此就需要在View中给对应的ViewModel添加一个相关的实例化方法。
注意:在这个教程管我们的Controller叫做”Views“,这笔“View”在MVVM更多语义。和UIKit使用的默认名不同。
打开 RWTFlickrSearchViewController.h 并声明ViewModel的头文件。
#import "RWTFlickrSearchViewModel.h"
然后加入下面的实例化方法
@interface RWTFlickrSearchViewController : UIViewController
- (instancetype)initWithViewModel:(RWTFlickrSearchViewModel *)viewModel;
@end
在RWTFlickrSearchViewController.m中添加一个私有变量
@property (weak, nonatomic) RWTFlickrSearchViewModel *viewModel;
接下来实现init方法
- (instancetype)initWithViewModel:(RWTFlickrSearchViewModel *)viewModel {
self = [super init];
if (self ) {
_viewModel = viewModel;
}
return self;
}
注意:这是个弱引用(弱指针),View引用了ViewModel,但没有拥有它。
在 viewDidLoad的最后加上下面代码
[self bindViewModel];
下面是这个方法的实现
- (void)bindViewModel {
self.title = self.viewModel.title;
self.searchTextField.text = self.viewModel.searchText;
}
上面的代码将会在UI初始化和ViewModel状态在VIew上应用的时候运行。
最后一步是实例化ViewModel,并在View中应用。
在viewDidLoad中添加以下
#import "RWTFlickrSearchViewModel.h"
加一个私有变量(在文件顶部的类扩展名内)。
@property (strong, nonatomic) RWTFlickrSearchViewModel *viewModel;
你会发现已经有了一个createInitialViewController方法,更新他的实现方法:
- (UIViewController *)createInitialViewController {
self.viewModel = [RWTFlickrSearchViewModel new];
return [[RWTFlickrSearchViewController alloc]initWithViewModel:self.viewModel];
}
这将创建一个新的ViewModel实例,然后它返回View。这是应用程序的导航控制器的初始视图。
恭喜你,这是你的第一个ViewModel。我得请你控制住你的兴奋!这里还有很多要学习。
你可能已经注意到了你没有使用任何ReactiveCocoa呢。在其目前的形式,任何用户进入搜索文本字段将不会反映在ViewModel。
检测有效搜索状态
在这一部分中,你将使用ReactiveCocoa绑定ViewModel和View的搜索框和按钮在一起,更新RWTFlickrSearchViewController.m中的bindViewModel方法如下
- (void)bindViewModel {
self.title = self.viewModel.title;
RAC(self.viewModel, searchText) = self.searchTextField.rac_textSignal;
}
我们来加一个ReactiveCocoa 中UITextFeild的分类的方法rac_textSignal,它是一个信号,每次文本字段更新时,将发出包含当前文本的一个事件,RAC宏是一个绑定,上面代码更新了ViewModel中的searchText对象的值,它随着rac_textSignal响应。
总之,上面的代码保证了searchText的值总是UI中的最新的值。如果上面的写法让你感到陌生,你真的应该重新学习一下ReactiveCocoa tutorials这个教程!
如果用户输入的文本是有效的,则只能启用搜索按钮。这里的输入规则是,他们必须输入超过三个字符,然后才能执行搜索。
在 RWTFlickrSearchViewModel.m加入下面代码
#import
更新方法initialize:
- (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);
}];
}
编译,运行并在TextFeild输入一些文字。每次文本在有效或无效状态之间转换时,都会看到日志消息:
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
上面的代码使用 RACObserve宏创建了一个ViewModel中的 searchText的信号。map操作将文本流转换为真值和假值。最后, distinctuntilchanges是用来确保该信号只在状态变化的时候传递值。