从 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。至于这两个宏具体生成什么代码,可以看文末附注。
@weakify(self);
[self.viewModel.updatedContentSignal subscribeNext:^(id x) {
@strongify(self);
[self.tableView reloadData];
}];
另外这几行代码的意思是如果信号 self.viewModel.updatedContentSignal 触发 next 事件并返回值,那么执行 subscribeNext 对应的 Block 代码。
而 ViewModel 的 updatedContentSignal 是我们在 ASHMasterViewModel 中自定义的信号:
@property (nonatomic, strong) RACSubject *updatedContentSignal;
我们在代码里手动触发这个信号的 next 事件:
[(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 里有这么一句:
// 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 里有这么一句:
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,这是干什么的呢?
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。
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:
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的文章翻译(翻译文章包含原文链接)
附注
@weakify(self); 宏实际上生成的代码是:
@autoreleasepool {} __attribute__((objc_ownership(weak))) __typeof__(self) self_weak_ = (self);;
@strongify(self); 宏实际上生成的代码是:
@autoreleasepool {} __attribute__((objc_ownership(strong))) __typeof__(self) self = self_weak_;
C-41与MVVM及ReactiveCocoa
本文通过分析C-41开源项目,详细介绍了MVVM架构模式和ReactiveCocoa框架在iOS开发中的应用。重点讨论了ViewModel层如何与View层和Model层交互,以及ReactiveCocoa如何简化这些交互过程。
874

被折叠的 条评论
为什么被折叠?



