前言
什么是MVVM模式?其实我觉得对于这个问题,每个人理解可能都是不一样的。因为ViewModel本身就是非常抽象的概念,它的实现也可以是各不相同。像是传统的MVC模式,需要把cell的设置放在ViewController中。实际上我们会更多的直接用cell调用Model,这虽然违背了MVC的原则,却使得整个项目结构变得清晰。所以个人认为大谈什么是V什么是M什么是VM没有很大的意义,重要的是对设计模式的合理运用,我会结合代码和Demo讲解我对MVVM模式的理解。另外,框架是基于ReactiveCocoa写的,我是默认你已经掌握了RAC相关的内容。
Model for View
我们先不谈ViewModel究竟是什么,先说一下在我的ViewModel里写了什么。在Demo里我创建了一个自定义的视图TitleView, 它有着以下两个Label以及一个Button:
@interface SUIMVVMRootTitleView : UIView
@property (weak, nonatomic) IBOutlet UILabel *lbl01;
@property (weak, nonatomic) IBOutlet UILabel *lbl02;
@property (weak, nonatomic) IBOutlet UIButton *clickBtn;
@end
两个Label的文字和颜色都可能会改变,同时Button点击了会触发事件,为了满足以上的需求ViewModel写了以下的属性:
@interface SUIMVVMRootTitleVM : SUIViewModel
@property (nonatomic,copy) NSString *text1;
@property (nonatomic,copy) NSString *text2;
@property (nonatomic,strong) UIColor *textColo1;
@property (nonatomic,strong) UIColor *textColo2;
@property (nonatomic,strong) RACCommand *clickCommand;
@end
简单来说,每一个自定义的View都对应了一个ViewModel,这个ViewModel里写的就是View改变的部分。在TitleView的.m里我把ViewModel和View进行了绑定:
@implementation SUIMVVMRootTitleView
- (Class)sui_classOfViewModel
{
return [SUIMVVMRootTitleVM class];
}
- (void)sui_bindWithViewModel
{
SUIVIEWBIND(SUIMVVMRootTitleVM,
RAC(self.lbl01, text) = SUIVIEWObserve(text1);
RAC(self.lbl02, text) = SUIVIEWObserve(text2);
RAC(self.lbl01, textColor) = SUIVIEWObserve(textColo1);
RAC(self.lbl02, textColor) = SUIVIEWObserve(textColo2);
)
self.clickBtn.rac_command = [self.sui_vm clickCommand];
}
@end
第一眼是不是完全看不懂,咳,让我们来一句句分析。
sui_classOfViewModel() 返回了当前View对应的ViewModel类型。在我的设计中,自定义的View只对应同一个ViewModel,将ViewModel设置为View的属性,同时使用懒加载模式创建实例。在ViewModel创建实例的时候它的类型就是由sui_classOfViewModel()的返回值决定的。相当于你在View初始化时创建了一个ViewModel并将它赋值给View中对应的属性。
sui_bindWithViewModel() 我在这个方法里使用了很多的宏,好处是可以少写很多的重复代码,坏处是无法在其中设置断点,也不能在宏内部使用LLDB。简单说一下每个宏的作用,为了调试方便可以直接展开来写。SUIVIEWBIND宏第一个参数填入了ViewModel的类名,告诉编译器ViewModel的类型,在第二个参数里用RAC将View和ViewModel绑定。SUIVIEWObserve是简单封装了RAC的RACObserve,也就是少写了RACObserve里的第一个参数直接填入ViewModel的属性。
至此View的代码部分已经完整了,大家可以发现根本就没有Model的事,如果需要复用这个View那么它本身的代码不需要做任何修改。View的职责就是响应ViewModel的改变,View所有的数据来源都是ViewModel,在我看来ViewModel其实就是"Model for View"。
Bind with Model
为了便于大家理解附上一个不重要的Model,说不重要的原因在于ViewModel虽然只绑定一个Model,但是它可以绑定不同的Model。换句话说,在ViewModel绑定了RootTitleMD后,再去绑定SecondTitleMD,那么在SecondTitleMD绑定前RootTitleMD会先解绑。
@interface SUIMVVMRootTitleMD : NSObject
@property (nonatomic,copy) NSString *kw;
@property (nonatomic) NSInteger numOfAlbums;
@end
着重看一下ViewModel的.m部分:
@implementation SUIMVVMRootTitleVM
- (void)sui_commonInit
{
SUIVMBIND(SUIMVVMRootTitleMD,
SUIVMRAC(text1, kw);
RAC(self, text2) = [SUIVMObserve(numOfAlbums)
map:^id(NSNumber *cNum) {
return gFormat(@"num:%@",cNum);
}];
RAC(self, textColo1) = [SUIVMObserve(numOfAlbums)
map:^id(id value) {
return gRandomColo;
}];
RAC(self, textColo2) = [SUIVMObserve(numOfAlbums)
map:^id(id value) {
return gRandomColo;
}];
)
}
@end
呃……好吧。就一段话,简单来看一下,感觉是不是和View中的代码很像。SUIVMBIND宏对应SUIVIEWBIND宏,SUIVMObserve宏对应SUIVIEWObserve,多了一个SUIVMRAC宏是极度偷懒使用的,展开其实就是:
RAC(self, text1) = SUIVMObserve(kw);
ViewModel的职责很明确,它做的其实就是胖Model做的事,关于胖Model限于篇幅这里就不多说了。大家可以看到Model里是不包含任何逻辑的,ViewModel负责对Model的数据加工,同时响应绑定Model的改变。
至此ViewModel的代码部分也已经完整了,再加上那个不重要的Model,一个 (V-VM)-M 版本的自定义视图就完成了。
……咦,等等,好像有哪里不对劲。你的Model是哪里来的啊?Model在哪里和ViewModel绑定的啊?在哪里响应Button的点击事件啊?你是把ViewController忘了么,完全没有提到啊喂?啊喂啊喂?
(V-VM)-M-(VM-V)
先来说一下ViewController,我们可以把ViewController看成View,依然配合Demo中的代码来看:
@interface SUIMVVMSecondVC ()
@property (weak, nonatomic) IBOutlet UIImageView *coverView;
@property (weak, nonatomic) IBOutlet UILabel *idLbl;
@end
@implementation SUIMVVMSecondVC
- (Class)sui_classOfViewModel
{
return [SUIMVVMSecondVM class];
}
- (void)viewDidLoad
{
[super viewDidLoad];
SUIVIEWBIND(SUIMVVMSecondVM,
@weakify(self)
[[SUIVIEWObserve(cover) ignore:nil]
subscribeNext:^(NSString *cCover) {
@strongify(self)
[self.coverView setImageWithURL:cCover.sui_toURL];
}];
RAC(self.idLbl, text) = SUIVIEWObserve(aId);
)
}
@end
和前面的View部分的代码对比你会发现,其实他们没有多少区别,就连用的宏都是一模一样的。接着我们再看一下另一段代码(删减了部分代码):
@interface SUIMVVMRootVC ()
@property (nonatomic,strong) SUIMVVMRootVM *sui_vm;
@property (weak, nonatomic) IBOutlet SUIMVVMRootTitleView *currTitleView;
@end
@implementation SUIMVVMRootVC
@dynamic sui_vm;
- (Class)sui_classOfViewModel
{
return [SUIMVVMRootVM class];
}
- (void)viewDidLoad
{
[super viewDidLoad];
// TitleView绑定model
[self.currTitleView.sui_vm bindWithModel:self.sui_vm.rootTitleMD];
// TitleView点击事件
uTypeof(SUIMVVMRootTitleVM, self.currTitleView.sui_vm).clickCommand = self.sui_vm.rootTitleClickCommand;
}
@end
我们的主角TitleView终于登场了,TitleView作为ViewController的属性,那么TitleView的ViewModel获取Model的代码(bindWithModel())就是写在ViewController中。换句话就是TitleView的持有者提供Model绑定给TitleView的ViewModel。
那么这里说的Model哪里来的?这个Model可以是网络数据模型,可以是本地数据模型,可以是从其他ViewController传递来的模型,我们的TitleView并不关心,TitleView只关心它对应的ViewModel的改变,ViewModel只关心绑定的Model的改变,TitleView的持有者绑定ViewModel和Model,大伙都各司其职。
综上,我们可以把ViewController看成View,但是ViewController还有另外一个职责就是获取Model,将它绑定给持有的TitleView对应的ViewModel,若TitleView内存在自定义视图则TitleView负责提供Model绑定给持有的自定义视图对应的ViewModel。
再来说一下Button的点击事件,大家可以看到我现在是把事件丢给ViewController处理(当然,懒惰的ViewController转手就丢给了它自己的ViewModel),我个人的看法是看需求吧,就像前言所说的只要合理就好。
最后的最后
限于篇幅,简单说一下一些其他的问题。
网络请求放在哪里?个人看法是封装成一个单独的模块,它和MVVM模式其实没有多大关系,一般是在ViewController的ViewModel里调用封装好的工具类得到Model。
TableView和CollectionView的代理?我为TableView写了一个Helper类,它的代理都丢过去了,具体可以看Demo。个人觉得再用代理或者Blocks回调到ViewController中处理其实不是很好。
那些宏看着好危险的感觉,能不能换成其他方式?如果用swift是可以的,因为swift是有重载特性,实现方式也会比现在好很多。然而OC不支持重载,因此我选择用宏来简化代码。展开来写会发现还做了移除Model绑定之类的处理,所以用宏最重要的目的是预防出错。
附上项目地址:https://github.com/randomprocess/SUIToolKit
这个框架主要是用来验证思路,有任何想法疑问或者Bug都欢迎提一个Issues。最后的最后弱弱的求个Star~