你可能在Twitter上看到过这样的笑话:
“iOS MVC架构,代表着杂乱无序的ViewController” - Colin Campbell
我敢确定大家开发中都碰到过,过大的、不可维护的view controller。
MVVM使用不同的架构模式,model-view-view model,简称MVVM。
这种模式,通过反射获得灵活性,是一个优秀的MVC替换模式,保证获得平滑的、轻量的view controller。
该MVVM教程从一个简单的flickr搜索应用开始,
在本教程开始前,我们回顾一下一些技术原理。
ReactiveCocoa扼要回顾
本教程是关于MVVM的重要文章,需要读者有一些ReactiveCocoa知识。
假如没有使用过ReactiveCocoa,强烈建议翻读我之前的ReactiveCocoa博客。
再次回顾一下ReactiveCocoa的要点,使读者对ReactiveCocoa有个快速概览。
ReactiveCocoa的核心是Signals,核心类RACSignal,Signals触发事件流(events stream), 包括类型,下一步,完成,以及错误等等。
使用这个简单的模式,ReactiveCocoa可以扮演委托模式(delegate pattern),行为目标模式(target-action patter), key-value模式,等等。
Signal API 代码干净,简单易读,而且在上层代码的很多操作中可以使用这些Signal。
你可以实现复杂的过滤器,发送模块,条件判断等高级设计模型。
在MVVM的上下文中,ReactiveCocoa扮演非常特俗的角色。它在ViewModel和View之间像提供了类似‘胶水’功能。
Model-View-ViewModel,即MVVM,是一个UI设计模式,是设计模式MV*家族中的一员,这些成员包括MVC,MVP(Model View Presenter),...。
上述的每个设计模式关注的是UI层和逻辑层(business logic)分离,为了使程序便于开发和测试。
Note:关于设计模式,我推荐Eli和Ash Furrow的文章。
为了更好理解MVVM,我们再看一下MVC。
MVC是第一个UI设计模式,它的原型来自70年代的SmallTalk语言。下面的图片展示了MVC模块的交互。
这个模式,Model维护着程序状态,UI控件、视图组成了View,而Controller处理了用户交互(Interactions),承担更新Model,View的事情。
一个大的问题关于MVC模式是太过模糊。原理看似简单,使用起来经常会使Model, View, Controller循环依赖。总之,越变越大,杂乱无章。
最近Martin Fowler介绍了MVC-Presentation Model, 就是采用了,并且微软也是倍加推崇的MVVM模式。
MVVM模式核心模块是ViewModel,这个特殊的model表示了应用程序的UI状态。
它包含了每个UI控件,视图的状态(state)和属性。比如,它包含了TextField的文本(text), 或者某个Button的enable属性,同样包含了视图(view)可以操作的事件(action),点击(tap),手势(gesture)。
可以认为ViewModel是视图的模型(Model-of-the-view)。
MVVM的三个组件和MVC的类似,遵循以下精确的规则,
1. 视图(View)有一个ViewModel的引用,相反没有;
2. ViewModel有一个Model的引用,相反没有;
以上两点需要一起执行,
MVVM显而易见的优点是,
1. 视图层更加轻,UI的逻辑都在ViewModel中;
2. 测试更加方便,应用拿掉UI也可以运行;
由于ViewModel没有View的引用,那如何更新View?重点来了,
MVVM 和 数据绑定(Data Binding)
MVVM模式依靠数据绑定(Data Binding), 一个框架层面的特性自动连接对象属性到UI控件。
例如,微软的WPF框架,下面的配置把TextField的Text属性和ViewModel中的Username绑定
<TextField Text=”{DataBinding Path=Username, Mode=TwoWay}”/>
TwoWay模块需要确保Username的改变能够传播到TextField的Text属性中,相反,用户输入时需要更新Username。
另外一个例子是Knockout(基于Web的MVVM框架),绑定的设置,
<input data-bind=”value: username”/>
一个HTML的元素和JavaScript模块中的元素绑定。
不幸的是iOS还缺少数据绑定框架,那么我们就用ReactiveCocoa来扮演这个角色。
从iOS开发的角度来看MVVM,ViewController和相关的UI-不管是nib,storyboard还是代码实现的,组成了View,
ReactiveCocoa绑定了View和ViewModel。
Note:推荐Martin Fowler的GUI Architectures article
Okey,我们开始创建一个ViewModel
Starter Project Sturecture
它用CocoaPods来管理依赖。
这个工程已经包含了应用程序的ViewController和nib文件组成的视图模块。
打开RWTFlickrSearch.xcworkspace。
花点时间熟悉一下工程结构,
第一个ViewModel
添加一个新类,命名为RWTFlickrSearchViewModel,继承自NSObject,
@interface RWTFlickrSearchViewModel : NSObject
@property (strong, nonatomic) NSString *searchText;
@property (strong, nonatomic) NSString *title;
@end
searchText属性代表者页面上TextField的text属性,title代表了导航栏上的标题。
Note:为了更好理解应用结构,View和ViewModel取相同的名称,带不同的后缀,RWTFlickrSearch-ViewModel和RWTFlickrSearch-ViewConrtoller。
@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和View关联。记住视图模块(View)有一个ViewModel的引用。看起来Viewmodel的初始化和视图模块关联起来也比较合理。
#import "RWTFlickrSearchViewModel.h"
@interface RWTFlickrSearchViewController : UIViewController
- (instancetype)initWithViewModel:(RWTFlickrSearchViewModel *)viewModel;
@end
在RWTFlickrSearchViewContrller.m中,添加一个私有的属性,
@property (weak, nonatomic) RWTFlickrSearchViewModel *viewModel;
- (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;
}
刚才提到的是在ViewController中保存了一个弱引用,那么在哪里拥有实例呢?
这个例子中提到的是AppDelegate中,
在RWTAppDelegate.m中,
#import "RWTFlickrSearchViewModel.h"
@property (strong, nonatomic) RWTFlickrSearchViewModel *viewModel;
看看初始化函数,
- (UIViewController *)createInitialViewController {
self.viewModel = [RWTFlickrSearchViewModel new];
return [[RWTFlickrSearchViewController alloc] initWithViewModel:self.viewModel];
}
ViewModel文件已经添加了,下面该ReactiveCocoa登场了。
Detecting Vaild Search State
接下来我们需要ReactiveCocoa把ViewModel和View绑定在一起了。
在RWTFlickrSearchViewController.m中,更新bindViewModel,
- (void)bindViewModel {
self.title = self.viewModel.title;
RAC(self.viewModel, searchText) = self.searchTextField.rac_textSignal;
}
添加一个rac_textSignal属性到UITextField类中,需要ReactiveCocoa来扩展。
当textfield更新后,就会触发包含当前text的一个事件(rac_textSignal)。
RAC宏就是viewModel中searchText和TextField rac_textSignal事件的一个绑定。
简而言之,确保searchText属性总是反映当前UI(TextField)的状态。
搜索按钮的enable状态和text输入是否有效关联在一起。在RWTFlickrSearchViewModel.m添加,
#import <ReactiveCocoa/ReactiveCocoa.h>
- (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);
}];
}
添加搜索命令
@property (strong, nonatomic) RACCommand *executeSearch;
- (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);
}];
self.executeSearch =
[[RACCommand alloc] initWithEnabled:validSearchSignal
signalBlock:^RACSignal *(id input) {
return [self executeSearchSignal];
}];
}
- (RACSignal *)executeSearchSignal {
return [[[[RACSignal empty]
logAll]
delay:2.0]
logAll];
}
现在该搜索按钮绑定了,
在RWTFlickrSearchViewController.m中
- (void)bindViewModel {
self.title = self.viewModel.title;
// 属性绑定
RAC(self.viewModel, searchText) = self.searchTextField.rac_textSignal;
// 搜索事件绑定
self.searchButton.rac_command = self.viewModel.executeSearch;
}
这个列子中,用Search TextField中的text搜索,并且返回一组匹配的结果。
你可以在ViewModel中添加逻辑模块。
RWTFlickrSearch.m提供接口定义,
#import <ReactiveCocoa/ReactiveCocoa.h>
@import Foundation;
@protocol RWTFlickrSearch <NSObject>
- (RACSignal *)flickrSearchSignal:(NSString *)searchString;
@end
RWTFlickrSearchImpl提供了实现部分,
@import Foundation;
#import "RWTFlickrSearch.h"
@interface RWTFlickrSearchImpl : NSObject <RWTFlickrSearch>
@end
implementation:
@implementation RWTFlickrSearchImpl
- (RACSignal *)flickrSearchSignal:(NSString *)searchString {
return [[[[RACSignal empty]
logAll]
delay:2.0]
logAll];
}
@end
RWTFlickrSearch详细的例子,方方面面都有涉及,请参考,
example project
“iOS MVC架构,代表着杂乱无序的ViewController” - Colin Campbell
我敢确定大家开发中都碰到过,过大的、不可维护的view controller。
MVVM使用不同的架构模式,model-view-view model,简称MVVM。
这种模式,通过反射获得灵活性,是一个优秀的MVC替换模式,保证获得平滑的、轻量的view controller。
该MVVM教程从一个简单的flickr搜索应用开始,
在本教程开始前,我们回顾一下一些技术原理。
ReactiveCocoa扼要回顾
本教程是关于MVVM的重要文章,需要读者有一些ReactiveCocoa知识。
假如没有使用过ReactiveCocoa,强烈建议翻读我之前的ReactiveCocoa博客。
再次回顾一下ReactiveCocoa的要点,使读者对ReactiveCocoa有个快速概览。
ReactiveCocoa的核心是Signals,核心类RACSignal,Signals触发事件流(events stream), 包括类型,下一步,完成,以及错误等等。
使用这个简单的模式,ReactiveCocoa可以扮演委托模式(delegate pattern),行为目标模式(target-action patter), key-value模式,等等。
Signal API 代码干净,简单易读,而且在上层代码的很多操作中可以使用这些Signal。
你可以实现复杂的过滤器,发送模块,条件判断等高级设计模型。
在MVVM的上下文中,ReactiveCocoa扮演非常特俗的角色。它在ViewModel和View之间像提供了类似‘胶水’功能。
Model-View-ViewModel,即MVVM,是一个UI设计模式,是设计模式MV*家族中的一员,这些成员包括MVC,MVP(Model View Presenter),...。
上述的每个设计模式关注的是UI层和逻辑层(business logic)分离,为了使程序便于开发和测试。
Note:关于设计模式,我推荐Eli和Ash Furrow的文章。
为了更好理解MVVM,我们再看一下MVC。
MVC是第一个UI设计模式,它的原型来自70年代的SmallTalk语言。下面的图片展示了MVC模块的交互。
这个模式,Model维护着程序状态,UI控件、视图组成了View,而Controller处理了用户交互(Interactions),承担更新Model,View的事情。
一个大的问题关于MVC模式是太过模糊。原理看似简单,使用起来经常会使Model, View, Controller循环依赖。总之,越变越大,杂乱无章。
最近Martin Fowler介绍了MVC-Presentation Model, 就是采用了,并且微软也是倍加推崇的MVVM模式。
MVVM设计模式
MVVM模式核心模块是ViewModel,这个特殊的model表示了应用程序的UI状态。
它包含了每个UI控件,视图的状态(state)和属性。比如,它包含了TextField的文本(text), 或者某个Button的enable属性,同样包含了视图(view)可以操作的事件(action),点击(tap),手势(gesture)。
可以认为ViewModel是视图的模型(Model-of-the-view)。
MVVM的三个组件和MVC的类似,遵循以下精确的规则,
1. 视图(View)有一个ViewModel的引用,相反没有;
2. ViewModel有一个Model的引用,相反没有;
以上两点需要一起执行,
MVVM显而易见的优点是,
1. 视图层更加轻,UI的逻辑都在ViewModel中;
2. 测试更加方便,应用拿掉UI也可以运行;
由于ViewModel没有View的引用,那如何更新View?重点来了,
MVVM 和 数据绑定(Data Binding)
MVVM模式依靠数据绑定(Data Binding), 一个框架层面的特性自动连接对象属性到UI控件。
例如,微软的WPF框架,下面的配置把TextField的Text属性和ViewModel中的Username绑定
<TextField Text=”{DataBinding Path=Username, Mode=TwoWay}”/>
TwoWay模块需要确保Username的改变能够传播到TextField的Text属性中,相反,用户输入时需要更新Username。
另外一个例子是Knockout(基于Web的MVVM框架),绑定的设置,
<input data-bind=”value: username”/>
一个HTML的元素和JavaScript模块中的元素绑定。
不幸的是iOS还缺少数据绑定框架,那么我们就用ReactiveCocoa来扮演这个角色。
从iOS开发的角度来看MVVM,ViewController和相关的UI-不管是nib,storyboard还是代码实现的,组成了View,
ReactiveCocoa绑定了View和ViewModel。
Note:推荐Martin Fowler的GUI Architectures article
Okey,我们开始创建一个ViewModel
Starter Project Sturecture
先下载初学者工程
它用CocoaPods来管理依赖。
这个工程已经包含了应用程序的ViewController和nib文件组成的视图模块。
打开RWTFlickrSearch.xcworkspace。
花点时间熟悉一下工程结构,
第一个ViewModel
添加一个新类,命名为RWTFlickrSearchViewModel,继承自NSObject,
@interface RWTFlickrSearchViewModel : NSObject
@property (strong, nonatomic) NSString *searchText;
@property (strong, nonatomic) NSString *title;
@end
searchText属性代表者页面上TextField的text属性,title代表了导航栏上的标题。
Note:为了更好理解应用结构,View和ViewModel取相同的名称,带不同的后缀,RWTFlickrSearch-ViewModel和RWTFlickrSearch-ViewConrtoller。
@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和View关联。记住视图模块(View)有一个ViewModel的引用。看起来Viewmodel的初始化和视图模块关联起来也比较合理。
#import "RWTFlickrSearchViewModel.h"
@interface RWTFlickrSearchViewController : UIViewController
- (instancetype)initWithViewModel:(RWTFlickrSearchViewModel *)viewModel;
@end
在RWTFlickrSearchViewContrller.m中,添加一个私有的属性,
@property (weak, nonatomic) RWTFlickrSearchViewModel *viewModel;
- (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;
}
刚才提到的是在ViewController中保存了一个弱引用,那么在哪里拥有实例呢?
这个例子中提到的是AppDelegate中,
在RWTAppDelegate.m中,
#import "RWTFlickrSearchViewModel.h"
@property (strong, nonatomic) RWTFlickrSearchViewModel *viewModel;
看看初始化函数,
- (UIViewController *)createInitialViewController {
self.viewModel = [RWTFlickrSearchViewModel new];
return [[RWTFlickrSearchViewController alloc] initWithViewModel:self.viewModel];
}
ViewModel文件已经添加了,下面该ReactiveCocoa登场了。
Detecting Vaild Search State
接下来我们需要ReactiveCocoa把ViewModel和View绑定在一起了。
在RWTFlickrSearchViewController.m中,更新bindViewModel,
- (void)bindViewModel {
self.title = self.viewModel.title;
RAC(self.viewModel, searchText) = self.searchTextField.rac_textSignal;
}
添加一个rac_textSignal属性到UITextField类中,需要ReactiveCocoa来扩展。
当textfield更新后,就会触发包含当前text的一个事件(rac_textSignal)。
RAC宏就是viewModel中searchText和TextField rac_textSignal事件的一个绑定。
简而言之,确保searchText属性总是反映当前UI(TextField)的状态。
搜索按钮的enable状态和text输入是否有效关联在一起。在RWTFlickrSearchViewModel.m添加,
#import <ReactiveCocoa/ReactiveCocoa.h>
- (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);
}];
}
添加搜索命令
@property (strong, nonatomic) RACCommand *executeSearch;
- (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);
}];
self.executeSearch =
[[RACCommand alloc] initWithEnabled:validSearchSignal
signalBlock:^RACSignal *(id input) {
return [self executeSearchSignal];
}];
}
- (RACSignal *)executeSearchSignal {
return [[[[RACSignal empty]
logAll]
delay:2.0]
logAll];
}
现在该搜索按钮绑定了,
在RWTFlickrSearchViewController.m中
- (void)bindViewModel {
self.title = self.viewModel.title;
// 属性绑定
RAC(self.viewModel, searchText) = self.searchTextField.rac_textSignal;
// 搜索事件绑定
self.searchButton.rac_command = self.viewModel.executeSearch;
}
Model在哪里?
这个列子中,用Search TextField中的text搜索,并且返回一组匹配的结果。
你可以在ViewModel中添加逻辑模块。
RWTFlickrSearch.m提供接口定义,
#import <ReactiveCocoa/ReactiveCocoa.h>
@import Foundation;
@protocol RWTFlickrSearch <NSObject>
- (RACSignal *)flickrSearchSignal:(NSString *)searchString;
@end
RWTFlickrSearchImpl提供了实现部分,
@import Foundation;
#import "RWTFlickrSearch.h"
@interface RWTFlickrSearchImpl : NSObject <RWTFlickrSearch>
@end
implementation:
@implementation RWTFlickrSearchImpl
- (RACSignal *)flickrSearchSignal:(NSString *)searchString {
return [[[[RACSignal empty]
logAll]
delay:2.0]
logAll];
}
@end
RWTFlickrSearch详细的例子,方方面面都有涉及,请参考,
example project
结论
- MVVM设计模式获得流行;
- MVVM模式可以把UI做的很轻很薄,增强了逻辑功能的可测性;
- 精确的规则View => ViewModel => Model,ViewModel和View之间用绑定实现。
- ViewModel不会持有View的引用,增强了View的可替换性。
- ViewModel可以想像成model-of-the-view, 它暴露了反映View的属性,还有用户交互的方法。
- Model层典型地用服务方式(Service)实现。
- 一个很好的MVVM应用是可以没有UI层就可以运行。
- ReactiveCocoa提供了很好的机制,使ViewModel和View绑定。
原文地址:
http://www.raywenderlich.com/74106/mvvm-tutorial-with-reactivecocoa-part-1
http://www.raywenderlich.com/74106/mvvm-tutorial-with-reactivecocoa-part-2