从 C-41 看 MVVM 和 ReactiveCocoa
2015-07-22 03:09
591 查看
从 C-41 看 MVVM 和 ReactiveCocoa
基本概念
C-41 是一个关于MVVM和
ReactiveCocoa的开源程序,我是通过 objc.io 上的一篇文章知道它的,相关地址:
英文版文章
中文版文章
项目地址
MVVM(
Model-View-ViewModel) 和
RAC(
ReactiveCocoa) 都有不错的介绍文章,前面提到的是一篇,其他的附在文章结尾介绍给大家。
阅读这篇文章是需要一点 MVVM 和 RAC 的基础的,完全不知道什么是 MVVM 或 RAC 的同学请先了解它们。
据我观察,MVVM 基本上是这么用的:一个 View/ViewController 对应一个 ViewModel,一个 ViewModel 通常只对应一个 Model,不过也可能聚合多个 Model(在这个程序中未出现)。如果一个 View/ViewController 想要对应不只一个 ViewModel,那就说明这个 View/ViewController 需要拆分成更细的部分,由更细的部分各自持有更细的 ViewModel。
文章差不多是按照我的代码阅读顺序写的,不过按照对
RAC的使用深度稍微调整了一下。
启动流程
ASHAppDelegate中,初始化了自定义的 CoreData 栈
ASHCoreDataStack,并为
ASHMasterViewController设置了 ViewModel。
这个程序中的 Model 全部都是依托于 CoreData 的数据类型,其实就两个
ASHRecipe和
ASHStep。
ASHMasterViewController的 ViewModel 作为
ASHMasterViewModel的实例,继承自
RVMViewModel,这是一个第三方为 RAC(
ReactiveCocoa)提供的 ViewModel 基类,可以使用 CocoaPods 集成到项目里。
RVMViewModel假定一个 ViewModel 只对应一个 Model。
然后程序就进入
ASHMasterViewController的控制范围。
ASHMasterViewController
和 ASHMasterViewModel
这个 ViewController 持有一个作为 Public 属性的 ViewModel, ASHMasterViewModel。
我们看到,ViewController 里要显示什么数据,都是直接从
self.viewModel里直接取,并没有做额外的处理,这使得 ViewController 瘦了很多,专注于处理 View 层的事情(输入相应、界面布局和动画等等)。
值得一提的是,在 ViewDidLoad 里,绑定了 ViewModel 的 updatedContentSignal 到一个 Block,
@weakify和
@strongify来自
libextobjc,用于解决 Block 引用的内存泄露问题,RAC 已经自带这个 Pod。至于这两个宏具体生成什么代码,可以看文末附注。
[code=language-objc]@weakify(self); [self.viewModel.updatedContentSignal subscribeNext:^(id x) { @strongify(self); [self.tableView reloadData]; }];
另外这几行代码的意思是如果信号
self.viewModel.updatedContentSignal触发
next事件并返回值,那么执行
subscribeNext对应的 Block 代码。
而 ViewModel 的
updatedContentSignal是我们在
ASHMasterViewModel中自定义的信号:
[code=language-objc]@property (nonatomic, strong) RACSubject *updatedContentSignal;
我们在代码里手动触发这个信号的
next事件:
[code=language-objc][(RACSubject *)self.updatedContentSignal sendNext:nil];
基本上这是一个比较标准的 TableViewController 子类,没有太多额外的内容。
接下来有几种方式跳转到其他 ViewController:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
无一例外,都是初始化了对应的 ViewController,然后设置它的 ViewModel。不过这里值得注意的是,下一层级的 ViewController 的 ViewModel,是由这一层级的 ViewController 的
self.viewModel获取的。
ASHEditRecipeViewController
和 ASHEditRecipeViewModel
ASHEditRecipeViewController又是一个 TableViewController,在
viewDidLoad里有这么一句:
[code=language-objc]// ReactiveCocoa Bindings RAC(self, title) = RACObserve(self.viewModel, name);
这就是为什么 MVVM 经常和 ReactiveCocoa 一起用的原因之一了,View 通常需要观察 ViewModel 的变化,在 ViewModel 变化的时候,自动更改 View 里的对应部分。这里就是让
self.titile自动反应
self.viewModel.name的变化。
另外在
-(void)configureTitleCell:(ASHTextFieldCell *)cell forIndexPath:(NSIndexPath *)indexPath里有这么一句:
[code=language-objc]RAC(self.viewModel, name) = [cell.textField.rac_textSignal takeUntil:cell.rac_prepareForReuseSignal];
我们发现赋值等号的右边不是用
RACObserve创建的Signal,而是使用
ReactiveCocoa对
textField做的扩展
rac_textSignal, 它实际上是创建了一个监听
textField的
UIControlEventEditingChanged事件的信号。
takeUntil:cell.rac_prepareForReuseSignal则是指只有当
cell的
-prepareForReuse被调用时才触发这个信号的
next或
completed事件。
ViewController 的其他部分一切如常,接下来我们看看
ASHEditRecipeViewModel。
-(instancetype)initWithModel:(id)model这个方法里有个RACChannelTo,这是干什么的呢?
[code=language-objc]RACChannelTo(self, name) = RACChannelTo(self.model, name); RACChannelTo(self, blurb) = RACChannelTo(self.model, blurb); RACChannelTo(self, filmType, @(ASHRecipeFilmTypeColourNegative)) = RACChannelTo(self.model, filmType, @(ASHRecipeFilmTypeColourNegative));
RACChannelTo(self, name) = RACChannelTo(self.model, name);这种写法是个双向绑定,也就是
self.name改变,
self.model.name会改变;反之
self.model.name改变的话,
self.name也会改变。
RACChannelTo(self, filmType, @(ASHRecipeFilmTypeColourNegative))里面第三个参数是指,如果值的变化中出现 nil,那么就会使用这个值来代替,相当于一个默认值。
这是为什么 MVVM 通常会依赖
ReactiveCocoa的原因之二,即 ViewModel 和 Model 的改变通常是需要双向同步的。
ASHDetailViewController
和 ASHDetailViewModel
ASHDetailViewController没什么好说的,我们看
ASHDetailViewModel。
[code=language-objc]RAC(self, canStartTimer) = [RACObserve(self.model, steps) map:^id(NSOrderedSet *value) { return @([value count] > 0); }];
这里出现了
map,对一个信号执行
map其实就是通过映射改变了它信号流下一步的值,即不再是原来 Observe 到的值。这里原先 Observe 到的值是
self.model.steps,是一个
NSOrderedSet,现在经过map,信号流的下一步收到的输入就是一个封装成
NSNumber的 BOOL 值,于是就和
self.canStartTimer对应起来了。这里信号流的概念就和 Unix 管道比较像,这一点应该在其他介绍
RAC或
响应式编程的文章中有所提及。
ASHTimerViewController
和 ASHTimerViewModel
ASHTimerViewController同样没什么好看的,我们看
ASHTimerViewModel:
[code=language-objc]RAC(self, nextStepString) = [RACSignal combineLatest:@[RACObserve(self.model, steps), RACObserve(self, currentStepIndex)] reduce:^id(NSOrderedSet *steps, NSNumber *currentStepIndexNumber) { NSInteger nextStepIndex = [currentStepIndexNumber integerValue] + 1; if (nextStepIndex >= 0 && nextStepIndex < steps.count) { return [[steps objectAtIndex:nextStepIndex] name]; } else { return @""; } }];
我们发现一个属性不仅仅只能绑定由单个值改变触发的信号,还可以绑定由多个值改变触发的聚合信号。通过
combineLatest:reduce:我们可以聚合多个信号成一个信号,让属性的改变是依赖多个值的变化的。
结尾
看到这里就差不多了,RAC有很多高级的特性,
MVVM也有一些更复杂的实现方式,而这个程序仅使用了比较基本的
MVVM结构和
RAC特性来构建,对于刚刚接触
MVVM和
RAC的 iOS 开发者来说,已经是一个上乘的例子,在很多地方都有提及。
我们回顾一下:在这个程序里,一个 ViewController(View层) 持有一个 ViewModel,一个 ViewModel 对应一个 Model。ViewController(View层) 对于 ViewModel 使用单向绑定,将 ViewModel 的变化反应到 ViewController(View层);ViewModel 对于 Model 使用双向绑定,不论修改 ViewModel 或是 Model 都会实现数据的同步更新。
于是我们把很多原本放在 ViewController 里的逻辑独立了出来,让属于 View层 的 ViewController 去做 View层 应该做的事情,而不要关心原本不属于它的事情。当然我们也没有把独立出来的这部分事情放在 Model 里,并不污染真正属于数据存储部分的逻辑。于是其实我们独立出来的这个部分,就成了 ViewModel。
其他参考文章
唐巧的技术博客: ReactiveCocoa - iOS开发的新框架iOS应用架构谈(二):View层的组织和调用方案(中)
Raywenderlich.com 上关于
MVVM和
ReactiveCocoa的文章翻译(翻译文章包含原文链接)
ReactiveCocoa指南一:信号
ReactiveCocoa指南二:Twitter搜索实例
MVVM指南一:Flickr搜索实例
MVVM指南二:Flickr搜索深入
附注
@weakify(self);宏实际上生成的代码是:
[code=language-objc]@autoreleasepool {} __attribute__((objc_ownership(weak))) __typeof__(self) self_weak_ = (self);;
@strongify(self);宏实际上生成的代码是:
[code=language-objc]@autoreleasepool {} __attribute__((objc_ownership(strong))) __typeof__(self) self = self_weak_;
相关文章推荐
- 从 C-41 看 MVVM 和 ReactiveCocoa
- ReactiveCocoa内存管理
- React 组件开发初探
- React Native环境安装
- React Native开发的通讯录应用
- Reactor Cooling - sgu 194 无源汇可行流
- react组件生命周期过程
- 说说ReactiveCocoa 2
- 高性能IO设计的Reactor和Proactor模式
- ReactNavtive框架教程(4)
- React JS快速入门教程
- reactor模式学习
- 从Angular转向React
- 说说React,Flux,Reray和GraghQL
- ReactEurope Conf 参会感想
- React Native
- ReactEurope Conf 参会感想
- Reactor和Proactor模式
- 学习React(一)
- 一天一工程总结系列-7.15-ReactiveCocoa