C-41 是一个关于 MVVM
和 ReactiveCocoa
的开源程序,我是经过 objc.io 上的一篇文章知道它的,相关地址:html
MVVM
(Model-View-ViewModel
) 和 RAC
(ReactiveCocoa
) 都有不错的介绍文章,前面提到的是一篇,其余的附在文章结尾介绍给你们。react
阅读这篇文章是须要一点 MVVM 和 RAC 的基础的,彻底不知道什么是 MVVM 或 RAC 的同窗请先了解它们。git
据我观察,MVVM 基本上是这么用的:一个 View/ViewController 对应一个 ViewModel,一个 ViewModel 一般只对应一个 Model,不过也可能聚合多个 Model(在这个程序中未出现)。若是一个 View/ViewController 想要对应不仅一个 ViewModel,那就说明这个 View/ViewController 须要拆分红更细的部分,由更细的部分各自持有更细的 ViewModel。github
文章差很少是按照个人代码阅读顺序写的,不过按照对 RAC
的使用深度稍微调整了一下。编程
ASHAppDelegate
中,初始化了自定义的 CoreData 栈 ASHCoreDataStack
,并为 ASHMasterViewController
设置了 ViewModel。架构
这个程序中的 Model 所有都是依托于 CoreData 的数据类型,其实就两个 ASHRecipe
和 ASHStep
。框架
ASHMasterViewController
的 ViewModel 做为 ASHMasterViewModel
的实例,继承自 RVMViewModel
,这是一个第三方为 RAC(ReactiveCocoa
)提供的 ViewModel 基类,可使用 CocoaPods 集成到项目里。 RVMViewModel
假定一个 ViewModel 只对应一个 Model。mvvm
而后程序就进入 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。
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_;