您的位置:首页 > Web前端 > React

从 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_;
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: