一次MVVM+ReactiveCocoa实践

http://www.cnblogs.com/manji/p/4846591.html

一次MVVM+ReactiveCocoa实践

前言

学习MVVM和ReactiveCocoa(简称RAC)也有一段时间了,不过都仅限于看博客,一直对这两个东西很感兴趣,觉得很创新,也一直想找个机会在项目中实践一下,但是还是有一些顾虑,毕竟没有实践过,网上的资料看的也有点云里雾里,实际上手可能还是有一定的难度。于是决定写一个简单的demo实践一下。我特意选择了一个刚刚写的项目中的一个界面来实现,为的是能从实际项目需求出发,看看换成MVVM+RAC该如何实现。(关于MVVM和ReactiveCocoa的基础介绍我这里就不在说了,网上有相关资料可以查阅)

所实现的功能

所实现的功能很简单,就一个列表界面,UITableView搞定,可以下拉刷新,上拉加载更多。最终的效果如下:

所采用的项目结构

Model:实体
View:Storyboard、xib和自定义view
ViewController:就是UIViewController了,我们要实现的界面对应的Controller就是ProductListViewController
viewmodel:(这个怎么翻译呢?视图实体?)你们懂的。
API:网络请求相关

用到的第三方库:

1 pod 'AFNetworking',~> 2.5.3'
2 pod ReactiveCocoa~> 2.53 pod MJRefresh~> 2.4.74 pod MJExtension~> 2.5.95 pod AFNetworking-RACExtensions~> 0.1.8'

除了AFNetworking和ReactiveCocoa,就是MJ大神的2个很受欢迎的类库了,都是很常用的吧。(此处容我做个悲伤的表情,我开始写这个demo的时候RAC3.0版本还只是alpha、beta版本,所以我用了2.0最终的一个正式版2.5,但是在写这篇文章的时候,我又pod search了一下,发现已经出到4.0alpha版本了,不知道4.0又有了哪些改动,但是我知道3.0版本里RACCommand被标记成了deprecate,由RACAction替代,用法应该差不多)

实现细节(MVVM与ReactiveCocoa结合)

获取列表数据

我们都知道在MVVM里,跟网络通信相关的操作都是应该由viewmodel来处理的,所以在ProductListviewmodel里定义了一个RACCommand,我们叫:

1 /** 2 * 获取数据Command 3 */ 4 @property (nonatomic,strong,readonly) RACCommand *fetchProductCommand;

viewmodel的init方法里对它进行初始化:

1 _fetchProductCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) { 2 3 return [[[apiclient sharedClient] 4 fetchProductWithPageIndex:@(1)] 5 takeuntil:self.cancelCommand.executionSignals]; 6 }];

订阅RACCommand,获取数据后赋值给items(items是保存所有数据的数组,即tableView的dataSource)

 1    @weakify(self);
 2     [[_fetchProductCommand.executionSignals switchToLatest] subscribeNext:^(ResponseData *response) {
 3         @strongify(self);
 4         if (!response.success) {
 5             [self.errors sendNext:response.error];
 6         }
 7         else {
 8             self.items = [ProductListModel objectArrayWithkeyvaluesArray:response.data];
 9             self.page = response.page;
10         }
11     }];

再看ProductListViewController里,订阅viewmodel的items,有变化时就reload tableview。

1 [RACObserve(self.viewmodel,items) subscribeNext:^(id x) { 2 @strongify(self); 3 [self.table reloadData]; 4 }];

tableView的dataSource如下:

1 #pragma mark - UITableViewDataSource 2 3 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 4 return 1; 5 } 6 7 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 8 return self.viewmodel.items.count; 9 } 10 11 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 12 ProductListCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ProductListCell" forIndexPath:indexPath]; 13 cell.viewmodel = [self.viewmodel itemviewmodelForIndex:indexPath.row]; 14 15 return cell; 16 }

再看自定义tableViewCell里:

1 - (id)initWithCoder:(NSCoder *)aDecoder { 2 self = [super initWithCoder:aDecoder]; 3 if (self) { 5 @weakify(self); 6 [RACObserve(self,viewmodel) subscribeNext:^( 7 8 @strongify(self); 9 self.productNameLabel.text = self.viewmodel.ProductName; 10 self.bankNameLabel.text = self.viewmodel.ProductBank; 11 self.profitLabel.text = self.viewmodel.ProductProfit; 12 self.saleStatusLabel.text = self.viewmodel.SaleStatusCn; 13 self.productTermlabel.text = self.viewmodel.ProductTerm; 14 self.productAmtLabel.text = self.viewmodel.ProductAmt; 15 16 }]; 17 } 18 19 return self; 20 }

有RAC就是这么方便,不要block回调,更无须delegate。

获取更多数据

上拉加载更多,MJ已经帮我们处理了。我们只需要在viewmodel里定义一个加载更多数据的RACCommand供调用即可。这里就不介绍了,具体可以看最终的demo。

UITableView 刷新状态切换

用过MJRefresh的都知道,不管是header还是footer,beginRefreshing后,获取完数据后是需要调用endRefreshing来切换刷新状态的。用RAC来实现的话,我们可以订阅RACCommand的executing信号,如下:

1 @weakify(self) 2 [_viewmodel.fetchProductCommand.executing subscribeNext:^(NSNumber *executing) { 3 NSLog(command executing:%@",executing); 4 if (!executing.boolValue) { 5 @strongify(self) 6 [self.table.header endRefreshing]; 7 } 8 }];

上面差不多就是viewmodel和ViewController之前的逻辑交互,他们之间就是通过ReactiveCocoa这座桥来连接的。

关于http请求这块,AFNetworking大家都比较熟悉用法了,AFNetworking-RACExtensions就是把AFNetworking里的http请求转成了RACSignal,在ReactiveCocoa的世界里,一切都是Signal(不知道说的对不对╮(╯_╰)╭)。

我封装了一个httpGet方法

1 - (RACSignal *)httpGet:(Nsstring *)URLString parameters:(id)parameters { 2 return [[[self rac_GET:URLString parameters:parameters] 3 catch:^RACSignal *(NSError *error) { 4 //对Error进行处理 5 NSLog(error:%@ 6 Todo: 这里可以根据error.code来判断下属于哪种网络异常,分别给出不同的错误提示 7 return [RACSignal error:[NSError errorWithDomain:ERROR" code:error.code userInfo:@{Success":@NO,0); line-height:1.5!important">Message":Bad Network!"}]]; 8 }] 9 reduceEach:^id(id responSEObject,NSURLResponse *response){ 10 NSLog(url:%@,resp:%@SEObject); 11 ResponseData *data = [ResponseData objectWithkeyvalues:responSEObject]; 12 13 return data; 14 }]; 15 }

里面主要干了两件事,第一是错误处理(下面会讲到),第二是对返回数据进行解析,一般都是把json数据转成Model。

在实际项目中,基本上所有api接口的返回值格式都是统一的(不统一的话你可以去打服务端的人了),所以我定义了一个叫ResponseData的Model,这个Model里有个NSObject类型的属性,用来接收不同类型的值(数组、对象(即字典)等)。这样的话每个api接口根据实际情况对这个NSObject类型的属性进行格式转换即可,使用起来就很方便了。

错误处理

错误处理又可以分好几种情况,比如:
1)网络错误(无网络,超时等)
2)服务器端错误(404、500等)
3)业务逻辑错误
前两种错误,都会进入RACCommand的errors信号通道,在上面封装的那个httpGet方法里可以看到,我们catch了error,然后就可以根据error的code来区分是哪种错误,这么区分的目的是给用户展示不同的错误提示,更加友好。
而第三种“错误”其实服务端返回的也是一个正常的json字符串,我们也是会将它解析成ResponseData对象,这个时候就得单独判断是否出现错误了。针对两种不同的情况,如果要分开处理,那必然会有很多重复的代码,作为一个追求高质量代码的程序猿来说,这是不可取的方案(甚至是不能忍的)。我的处理方案是(参考了http://limboy.me/ios/2014/06/06/deep-into-reactivecocoa2.html中关于RACSubject的用法):

1)定义一个Baseviewmodel作为所有viewmodel的基类

@interface Baseviewmodel : NSObject 3 @property (nonatomic) RACSubject *errors; 4 5 6 * 取消请求Command 7 8 @property (nonatomic,255); line-height:1.5!important">readonly) RACCommand *cancelCommand; 9 10 @end

2)对RACCommand的errors进行合并:

1 [[RACSignal merge:@[_fetchProductCommand.errors,self.fetchMoreProductCommand.errors]] subscribe:self.errors];

3)在RACCommand的订阅里判断是否出现error,如果有错误,手动send一个error。

1   @weakify(self);

4)ViewController里对viewmodel里的errors进行订阅

1 [_viewmodel.errors subscribeNext:^(NSError *error) { 2 ResponseData *data = [ResponseData objectWithkeyvalues:error.userInfo]; something error:%@keyvalues); Todo: 这里可以选择一种合适的方式将错误信息展示出来 5 }];

原则就是把所有的错误都统一到一个通道里,这样只需要在一个地方处理就行了。

http请求cancel

我们在实现某些界面功能时,往往会在界面打开后进行http请求,有时会显示一个指示器告诉用户正在请求数据。但是如果网络比较差的情况下(比如2G网),有时用户可能觉得等的时间太长了,就点了返回,界面虽然是关闭了,但是对于那个http请求来说它还在继续的。这个时候比较好的处理方式就是将那个http请求cancel掉。不用RAC的情况下,我们需要记录每次发起http请求的NSURLSessionTask(如果你是用的AFNetworking的AFHTTPSessionManager的话),然后在Viewcontroller的dealloc里调用【task cancel】来取消这个task,需要注意的时,task被cancel的时候会返回error,这个时候就需要判断下errorCode来甄别是不是cancel,以免跟其他网络异常弄混。
那么用ReactiveCocoa该怎么实现http的cancel呢?好在AFNetworking-RACExtensions’已经帮我们封装好了,我们只需要在viewmodel里定义一个表示取消http请求的RACCommand(可以放到Baseviewmodel里),然后再必要的地方调用这个command即可,当然前提是我们在发起http请求的command里设置了如下的代码

6 }];

核心点就在于takeuntil,它表示“一直执行直到…”,套用在我们这里就是http请求一直执行,直到cancel命令被下达。经过测试可以发现完全能达到我们的目的。
PS:这里额外介绍下如何模拟不稳定的网络。设置 -> 开发者 -> NETWORK LINK CONDITIONER,里面有各种选项可供选择,比如100% Loss,3G,Very Bad Network等,虽然没有专业工具那么强大,但是简单模拟下异常网络也是足够了。

Model与viewmodel的界定

这两者关系说清晰也清晰,说不清晰也不清晰。

为什么说清晰呢?因为Model是实体,一般就是一些属性字段而已,而viewmodel是介于ViewController于Model之间的桥梁,viewmodel里有RACCommand,也会有一些业务逻辑(比如分页处理,ViewController只需要调用fetchData或者fetchMoreData即可,无需知道现在显示的是第几页)。

那为什么又不清晰呢?在我这个demo里有个自定义tablecell的viewmodel(ProductListCellviewmodel),这里面其实也就是一些属性而已,跟ProductListModel基本上都是一样的。所以遇到这种情况就比较迷惑,到底是拿Model当viewmodel用呢,还是分开冗余一部分代码呢?而且http请求返回的数据一般就是ViewController需要显示的数据(只是一般情况,也有需要额外处理的)。

到底该怎么处理呢?说说我的理解:
1)从http请求获得的数据,就是sourceData,而我们的Model就是作为sourceData而存在的,所以我更倾向于用Model来映射json数据。
2)viewmodel是拿到Model进行处理(有时可能不需要额外处理),然后提供给ViewController使用,比如直接显示到View上。

这也真是MVVM框架的核心。所以viewmodel里的items保存的是Model的数组。那么问题又来了,既然items里是Model,而ViewController又是通过viewmodel获取sourceData,那从Model到viewmodel该在哪里进行转换呢?

我能想到的是3个方案:
1)使用Model解析json数据后,循环遍历Model转成viewmodel保存到items里。这种做法,items里保存的是viewmodel而不是Model,TableCell使用的时候直接拿items里的viewmodel即可。
2)items保存Model,TableCell直接使用Model。当Model跟viewmodel几乎完全一致的情况下很有可能会出现这种情况。因为会觉得完全复制一个viewmodel出来不值,但是这又不太符合MVVM。
3)items保存Model,TableCell获取viewmodel时,通过Model初始化viewmodel。
我目前使用的是第3种方案,在viewmodel里使用Model作为一个属性,然后提供一些readonly的属性并重写其get方法(中间可以对数据进行一些格式化之类的)供界面使用。

遇到的坑

独自学习RAC还是有一定的难度的,毕竟面对众多RAC的api要想完全理解下来还是挺困难的。而且刚开始不熟悉的情况下很难针对某些特定的场景,想出比较合理的RAC处理方式(这句话是盗用别人的,但是我也深有体会)。

这里列一下我写这个demo时遇到的几个坑吧,希望能帮别人绕过这些坑,也算是功德一件。
1)viewmodel里用来保存数据的数组,不能使用NSMutableArray。原因是RAC是基于KVO的,而NSMutableArray的Add和Remove方法并不会给KVO发送通知,因此对NSMutableArray进行RACObserve时,并不会达到我们想要的结果。(同理其他Mutable的也都不能用)
2)viewmodel里给items赋值时,不能用_items=somearray,而是得用self.items。我开始是想在viewmodel里定义一个readonly的items属性(理论上也应该是readonly的,因为ViewController只负责从viewmodel拿数据而已),然后通过_items进行赋值,但是订阅viewmodel的items后死活收不到消息。我一直感觉这不科学,也许是我的打开方式不对,但是最终都没有解决。这里希望知道的人能不吝赐教,在下感激不尽。
3)实现可以cancel的http请求时,不能用replay,replayLast,replayLazily。关于这3者的区分可以参考这个,我觉得分析的很详细。

总结

以上就是我的一次MVVM+RAC的实践,初学MVVM和RAC,难免有些概念和理解有偏差,欢迎批评指正,也欢迎一起交流讨论。为的是能更好的学习和进步!

这里奉上我的demo源码:传送门

(因为demo所用接口是实际项目接口,容我将其抹掉)

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

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom