从 C-41 看 MVVM 和 ReactiveCocoa

从 C-41 看 MVVM 和 ReactiveCocoa

基本概念

C-41 是一个关于 MVVMReactiveCocoa 的开源程序,我是通过 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 的数据类型,其实就两个 ASHRecipeASHStep

ASHMasterViewController 的 ViewModel 作为 ASHMasterViewModel 的实例,继承自 RVMViewModel,这是一个第三方为 RAC(ReactiveCocoa)提供的 ViewModel 基类,可以使用 CocoaPods 集成到项目里。 RVMViewModel 假定一个 ViewModel 只对应一个 Model。

然后程序就进入 ASHMasterViewController 的控制范围。

ASHMasterViewControllerASHMasterViewModel

这个 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 获取的。

ASHEditRecipeViewControllerASHEditRecipeViewModel

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,而是使用 ReactiveCocoatextField 做的扩展 rac_textSignal,它实际上是创建了一个监听 textFieldUIControlEventEditingChanged 事件的信号。 takeUntil:cell.rac_prepareForReuseSignal 则是指只有当 cell-prepareForReuse被调用时才触发这个信号的 nextcompleted 事件。

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 的改变通常是需要双向同步的。

ASHDetailViewControllerASHDetailViewModel

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响应式编程 的文章中有所提及。

ASHTimerViewControllerASHTimerViewModel

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 特性来构建,对于刚刚接触 MVVMRAC 的 iOS 开发者来说,已经是一个上乘的例子,在很多地方都有提及。

我们回顾一下:在这个程序里,一个 ViewController(View层) 持有一个 ViewModel,一个 ViewModel 对应一个 Model。ViewController(View层) 对于 ViewModel 使用单向绑定,将 ViewModel 的变化反应到 ViewController(View层);ViewModel 对于 Model 使用双向绑定,不论修改 ViewModel 或是 Model 都会实现数据的同步更新。

于是我们把很多原本放在 ViewController 里的逻辑独立了出来,让属于 View层 的 ViewController 去做 View层 应该做的事情,而不要关心原本不属于它的事情。当然我们也没有把独立出来的这部分事情放在 Model 里,并不污染真正属于数据存储部分的逻辑。于是其实我们独立出来的这个部分,就成了 ViewModel。

其他参考文章

附注

@weakify(self); 宏实际上生成的代码是:

@autoreleasepool {} __attribute__((objc_ownership(weak))) __typeof__(self) self_weak_ = (self);;

@strongify(self); 宏实际上生成的代码是:

@autoreleasepool {} __attribute__((objc_ownership(strong))) __typeof__(self) self = self_weak_;

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


我正在用TitaniumDeveloper编写一个应用程序,它允许我使用Javascript,PHP,Ruby和Python.它为API提供了一些可能需要的标准功能,但缺少的是全局事件.现在我想将全局热键分配给我的应用程序并且几乎没有任何问题.现在我只针对MAC,但无法找到任何Python或Ruby的解决方案.我找到了Coc
我的问题是当我尝试从UIWebView中调用我的AngularJS应用程序中存在的javascript函数时,该函数无法识别.当我在典型的html结构中调用该函数时,该函数被识别为预期的.示例如下:Objective-C的:-(void)viewDidLoad{[superviewDidLoad];//CODEGOESHERE_webView.d
我想获取在我的Mac上运行的所有前台应用程序的应用程序图标.我已经使用ProcessManagerAPI迭代所有应用程序.我已经确定在processMode中设置了没有modeBackgroundOnly标志的任何进程(从GetProcessInformation()中检索)是一个“前台”应用程序,并显示在任务切换器窗口中.我只需要
我是一名PHP开发人员,我使用MVC模式和面向对象的代码.我真的想为iPhone编写应用程序,但要做到这一点我需要了解Cocoa,但要做到这一点我需要了解Objective-C2.0,但要做到这一点我需要知道C,为此我需要了解编译语言(与解释相关).我应该从哪里开始?我真的需要从简单的旧“C”开始,正
OSX中的SetTimer在Windows中是否有任何等效功能?我正在使用C.所以我正在为一些软件编写一个插件,我需要定期调用一个函数.在Windows上,我只是将函数的地址传递给SetTimer(),它将以给定的间隔调用.在OSX上有一个简单的方法吗?它应该尽可能简约.我并没有在网上找到任何不花哨的东西
我不确定引擎盖下到底发生了什么,但这是我的设置,示例代码和问题:建立:>雪豹(10.6.8)>Python2.7.2(由EPD7.1-2提供)>iPython0.11(由EPD7.1-2提供)>matplotlib(由EPD7.1-2提供)示例代码:importnumpyasnpimportpylabasplx=np.random.normal(size=(1000,))pl.plot
我正在使用FoundationFramework在Objective-C(在xCode中)编写命令行工具.我必须使用Objective-C,因为我需要取消归档以前由NSKeyedArchiver归档的对象.我的问题是,我想知道我现在是否可以在我的Linux网络服务器上使用这个编译过的应用程序.我不确定是否会出现运行时问题,或者可
使用cocoapods,我们首先了解一下rvm、gem、ruby。rvm和brew一样,但是rvm是专门管理ruby的版本控制的。rvmlistknown罗列出ruby版本rvminstall版本号   可以指定更新ruby版本而gem是包管理gemsource-l查看ruby源gemsource-rhttps://xxxxxxxx移除ruby源gemsou
我有一个包含WebView的Cocoa应用程序.由于应用程序已安装客户群,我的目标是10.4SDK.(即我不能要求Leopard.)我有两个文件:index.html和data.js.在运行时,为了响应用户输入,我通常会使用应用程序中的当前数据填充data.js文件.(data.js文件由body.html上的index.html文件用于填充
如何禁用NSMenuItem?我点击后尝试禁用NSMenuItem.操作(注销)正确处理单击.我尝试通过以下两种方式将Enabled属性更改为false:partialvoidLogout(AppKit.NSMenuItemsender){sender.Enabled=false;}和partialvoidLogout(AppKit.NSMenuItemsender){LogoutI
我在想,创建一个基本上只是一个带Web视图的界面的Cocoa应用程序是否可行?做这样的事情会有一些严重的限制吗?如果它“可行”,那是否也意味着你可以为Windows应用程序做同样的事情?解决方法:当然可以创建一个只是一个Cocoa窗口的应用程序,里面有一个Web视图.这是否可以被称为“可可应
原文链接:http://www.cnblogs.com/simonshi2012/archive/2012/10/08/2715464.htmlFrom:http://www.idev101.com/code/Cocoa/Notifications.htmlNotificationsareanincrediblyusefulwaytosendmessages(anddata)betweenobjectsthatotherwi
如果不手动编写GNUmake文件,是否存在可以理解Xcode项目的任何工具,并且可以直接针对GNUstep构建它们,从而生成Linux可执行文件,从而简化(略微)保持项目在Cocoa/Mac和GNUstep/Linux下运行所需的工作?基本上,是否有适用于Linux的xcodebuild样式应用程序?几个星期前我看了pbtomake
我正在将页面加载到WebView中.该页面有这个小测试Javascript:<scripttype="text/javascript">functiontest(parametr){$('#testspan').html(parametr);}varbdu=(function(){return{secondtest:function(parametr){$('#testspan&#039
我正在尝试使用NSAppleScript从Objective-C执行一些AppleScript…但是,我正在尝试的代码是Yosemite中用于自动化的新JavaScript.它在运行时似乎没有做任何事情,但是,正常的AppleScript工作正常.[NSAppactivateIgnoringOtherApps:YES];NSAppleScript*scriptObject=[[NSApple
链接:https://pan.baidu.com/s/14_im7AmZ2Kz3qzrqIjLlAg           vjut相关文章Python与Tkinter编程ProgrammingPython(python编程)python基础教程(第二版)深入浅出PythonPython源码剖析Python核心编程(第3版)图书信息作者:Kochan,StephenG.出
我正在实现SWTJava应用程序的OSX版本的视图,并希望在我的SWT树中使用NSOutlineView提供的“源列表”选项.我通过将此代码添加到#createHandle()方法来破解我自己的Tree.class版本来实现这一点:longNSTableViewSelectionHighlightStyleSourceList=1;longhi=OS.sel_regist
我的Cocoa应用程序需要使用easy_install在用户系统上安装Python命令行工具.理想情况下,我想将一个bash文件与我的应用程序捆绑在一起然后运行.但据我所知这是不可能的,因为软件包安装在Python的“site-packages”目录中.有没有办法创建这些文件的“包”?如果没有,我应该如何运行ea
摘要: 文章工具 收藏 投票评分 发表评论 复制链接 Swing 是设计桌面应用程序的一个功能非常强大工具包,但Swing因为曾经的不足常常遭到后人的诟病.常常听到旁人议论纷纷,”Swing 运行太慢了!”,”Swing 界面太丑嘞”,甚至就是说”Swing 简直食之无味”. 从Swing被提出到现在,已是十年光景,Swing早已不是昔日一无是处的Swing了. Chris Adamson 和我写
苹果的开发:   我对于Linux/Unix的开发也是一窍不通,只知道可以用Java.不过接触了苹果过后,确实发现,世界上确实还有那么一帮人,只用苹果,不用PC的.对于苹果的开发,我也一点都不清楚,以下是师兄们整理出来的网站. http://www.chezmark.com/osx/    共享软件精选 http://www.macosxapps.com/    分类明了,更新及时的一个重要Mac