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 BindingsRAC(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,@(ASHRecipeFilmTypeColourNegative));
RACChannelTo(self,name);
这种写法是个双向绑定,也就是 self.name
改变,self.model.name
会改变;反之 self.model.name
改变的话,self.name
也会改变。
RACChannelTo(self,@(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
的文章翻译(翻译文章包含原文链接) ReactiveCocoa指南一:信号 ReactiveCocoa指南二:Twitter搜索实例 MVVM指南一:Flickr搜索实例 MVVM指南二:Flickr搜索深入 附注 @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所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)