一次MVVM+ReactiveCocoa实践
前言
学习MVVM和ReactiveCocoa(简称RAC)也有一段时间了,不过都仅限于看博客,一直对这两个东西很感兴趣,觉得很创新,也一直想找个机会在项目中实践一下,但是还是有一些顾虑,毕竟没有实践过,网上的资料看的也有点云里雾里,实际上手可能还是有一定的难度。于是决定写一个简单的demo实践一下。我特意选择了一个刚刚写的项目中的一个界面来实现,为的是能从实际项目需求出发,看看换成MVVM+RAC该如何实现。(关于MVVM和ReactiveCocoa的基础介绍我这里就不在说了,网上有相关资料可以查阅)
所实现的功能
所实现的功能很简单,就一个列表界面,UITableView搞定,可以下拉刷新,上拉加载更多。最终的效果如下:
所采用的项目结构
Model:实体
View:Storyboard、xib和自定义view
ViewController:就是UIViewController了,我们要实现的界面对应的Controller就是ProductListViewController
ViewModel:(这个怎么翻译呢?视图实体?)你们懂的。
API:网络请求相关
用到的第三方库:
pod 'AFNetworking', '~> 2.5.3'
pod 'ReactiveCocoa', '~> 2.5'
pod 'MJRefresh', '~> 2.4.7'
pod 'MJExtension', '~> 2.5.9'
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,我们叫:
/**
* 获取数据Command
*/
@property (nonatomic, strong, readonly) RACCommand *fetchProductCommand;
在ViewModel的init方法里对它进行初始化:
_fetchProductCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) { return [[[APIClient sharedClient]
fetchProductWithPageIndex:@()]
takeUntil:self.cancelCommand.executionSignals];
}];
订阅RACCommand,获取数据后赋值给items(items是保存所有数据的数组,即tableView的dataSource)
@weakify(self);
[[_fetchProductCommand.executionSignals switchToLatest] subscribeNext:^(ResponseData *response) {
@strongify(self);
if (!response.success) {
[self.errors sendNext:response.error];
}
else {
self.items = [ProductListModel objectArrayWithKeyValuesArray:response.data];
self.page = response.page;
}
}];
再看ProductListViewController里,订阅ViewModel的items,有变化时就reload tableview。
[RACObserve(self.viewModel, items) subscribeNext:^(id x) {
@strongify(self);
[self.table reloadData];
}];
tableView的dataSource如下:
#pragma mark - UITableViewDataSource - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return ;
} - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.viewModel.items.count;
} - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
ProductListCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ProductListCell" forIndexPath:indexPath];
cell.viewModel = [self.viewModel itemViewModelForIndex:indexPath.row]; return cell;
}
再看自定义tableViewCell里:
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder]; if (self) {
@weakify(self);
[RACObserve(self, viewModel) subscribeNext:^(id x) { @strongify(self);
self.productNameLabel.text = self.viewModel.ProductName;
self.bankNameLabel.text = self.viewModel.ProductBank;
self.profitLabel.text = self.viewModel.ProductProfit;
self.saleStatusLabel.text = self.viewModel.SaleStatusCn;
self.productTermLabel.text = self.viewModel.ProductTerm;
self.productAmtLabel.text = self.viewModel.ProductAmt; }];
} return self;
}
有RAC就是这么方便,不要block回调,更无须delegate。
获取更多数据
上拉加载更多,MJ已经帮我们处理了。我们只需要在ViewModel里定义一个加载更多数据的RACCommand供调用即可。这里就不介绍了,具体可以看最终的demo。
UITableView 刷新状态切换
用过MJRefresh的都知道,不管是header还是footer,beginRefreshing后,获取完数据后是需要调用endRefreshing来切换刷新状态的。用RAC来实现的话,我们可以订阅RACCommand的executing信号,如下:
@weakify(self)
[_viewModel.fetchProductCommand.executing subscribeNext:^(NSNumber *executing) {
NSLog(@"command executing:%@", executing);
if (!executing.boolValue) {
@strongify(self)
[self.table.header endRefreshing];
}
}];
上面差不多就是ViewModel和ViewController之前的逻辑交互,他们之间就是通过ReactiveCocoa这座桥来连接的。
关于http请求这块,AFNetworking大家都比较熟悉用法了,AFNetworking-RACExtensions就是把AFNetworking里的http请求转成了RACSignal,在ReactiveCocoa的世界里,一切都是Signal(不知道说的对不对╮(╯_╰)╭)。
我封装了一个httpGet方法:
- (RACSignal *)httpGet:(NSString *)URLString parameters:(id)parameters {
return [[[self rac_GET:URLString parameters:parameters]
catch:^RACSignal *(NSError *error) {
//对Error进行处理
NSLog(@"error:%@", error);
//TODO: 这里可以根据error.code来判断下属于哪种网络异常,分别给出不同的错误提示
return [RACSignal error:[NSError errorWithDomain:@"ERROR" code:error.code userInfo:@{@"Success":@NO, @"Message":@"Bad Network!"}]];
}]
reduceEach:^id(id responseObject, NSURLResponse *response){
NSLog(@"url:%@,resp:%@",response.URL.absoluteString,responseObject);
ResponseData *data = [ResponseData objectWithKeyValues:responseObject]; return data;
}];
}
里面主要干了两件事,第一是错误处理(下面会讲到),第二是对返回数据进行解析,一般都是把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 @property (nonatomic) RACSubject *errors; /**
* 取消请求Command
*/
@property (nonatomic, strong, readonly) RACCommand *cancelCommand; @end
2)对RACCommand的errors进行合并:
[[RACSignal merge:@[_fetchProductCommand.errors, self.fetchMoreProductCommand.errors]] subscribe:self.errors];
3)在RACCommand的订阅里判断是否出现error,如果有错误,手动send一个error。
@weakify(self);
[[_fetchProductCommand.executionSignals switchToLatest] subscribeNext:^(ResponseData *response) {
@strongify(self);
if (!response.success) {
[self.errors sendNext:response.error];
}
else {
self.items = [ProductListModel objectArrayWithKeyValuesArray:response.data];
self.page = response.page;
}
}];
4)ViewController里对ViewModel里的errors进行订阅。
[_viewModel.errors subscribeNext:^(NSError *error) {
ResponseData *data = [ResponseData objectWithKeyValues:error.userInfo];
NSLog(@"something error:%@", data.keyValues);
//TODO: 这里可以选择一种合适的方式将错误信息展示出来
}];
原则就是把所有的错误都统一到一个通道里,这样只需要在一个地方处理就行了。
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里设置了如下的代码:
_fetchProductCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) { return [[[APIClient sharedClient]
fetchProductWithPageIndex:@()]
takeUntil:self.cancelCommand.executionSignals];
}];
核心点就在于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所用接口是实际项目接口,容我将其抹掉)
一次MVVM+ReactiveCocoa实践的更多相关文章
- WPF自定义控件与样式(14)-轻量MVVM模式实践
一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. MVVM是WPF中一个非 ...
- ReactiveCocoa实践
1.按钮addTarget [[self.aDepositBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNe ...
- MVVM With ReactiveCocoa让移动开发更简单
作者:@雷纯锋2011 MVVM是一种软件架构模式,它是 Martin Fowler 的 Presentation Model 的一种变体,最先由微软的架构师 John Gossman 在 2005 ...
- 最快让你上手ReactiveCocoa之进阶篇
前言 由于时间的问题,暂且只更新这么多了,后续还会持续更新本文<最快让你上手ReactiveCocoa之进阶篇>,目前只是简短的介绍了些RAC核心的一些方法,后续还需要加上MVVM+Rea ...
- (WPF) MVVM: 动态添加控件及绑定。
比如需要显示一个键盘,里面有各个按键.实现的效果如下: 之前的思路,就是建立一个singleKey的控件,然后在后台用代码动态的添加到父控件里去, 再用代码在后台进行绑定. 这种实现方法并不是真正的M ...
- [HMLY]11.MVVM架构
概要 MVC架构,Model-View-Controller,如图一所示为一个典型的MVC设置. 图一:mvc Model呈现数据 View呈现用户界面 Controller调节两者之间的交互.从Mo ...
- 谈MVVM
什么是MVVM? MVVM(模型-视图-视图模型,Model-View-ViewModal)是一种架构模式,并非一种框架,它是一种思想,一种组织与管理代码的艺术.它利用数据绑定,属性依赖,路由事件,命 ...
- 前端框架MVVM是什么(整理)
前端框架MVVM是什么(整理) 一.总结 一句话总结:vm层(视图模型层)通过接口从后台m层(model层)请求数据,vm层继而和v(view层)实现数据的双向绑定. 1.我大前端应该不应该做复杂的数 ...
- 架构-MVVM:MVVM核心概念
ylbtech-架构-MVVM:MVVM核心概念 1.返回顶部 1. MVVM模式是Model.View.ViewModel的简称,最早出现在WPF,现在Silverlight中也使用该模式,MVVM ...
随机推荐
- codeforces 377A. Puzzles 水题
A. Puzzles Time Limit: 20 Sec Memory Limit: 256 MB 题目连接 http://codeforces.com/problemset/problem/33 ...
- C语言统计一个字符串中单词的个数
假定每一个单词用空格隔开. 样例: 输入:how are you! 输出:3 两种方法: 一: #include <stdio.h> #include <string.h> # ...
- android安全:forceStopPackage对android的Alarm的影响
也许一些使用alarmmanager做定时任务的同学遇到过这样的问题:设定alarm后,进入设置-->应用程序管理-->强行停止app后,定时任务就失效了. 简单的讲就是:force st ...
- Android Compatibility package 兼容性开发套件
我们认为Android 3.0平板电脑操作系统在美国时间2011年2月22日的正式推出,对于Android手机应用程序开发者所象征的意涵是: 之前大家所开发过的Android手机应用,除了可以在And ...
- Flex中NetConnection与NetStream的关系、及浏览器并发连接数测试[转]
最近在做一个基于BS结构的视频会议系统,决定采用开源的FluorineFx.net与Flex结合的方法进行开发,前期开发都非常顺利,包括同步白板等.但到了实时视频传输的时候,原本设计是每个客户端可以显 ...
- DOS攻击之详解--转载
源地址没有找到,间接引用地址:http://wushank.blog.51cto.com/3489095/1156004 DoS到底是什么?接触PC机较早的同志会直接想到微软磁盘操作系统的DOS--D ...
- MAMP Pro3.5注册码
MAMP这个就不用介绍了,堪称MAC下的苏菲玛索,官方下载地址:https://www.mamp.info/en/mamp-pro/ ,400多大洋,土豪朋友请直接购买吧,正版还是要支持的. 和我 ...
- Javascript Date原型方法
// 对Date的扩展,将 Date 转化为指定格式的String // 月(M).日(d).小时(h).分(m).秒(s).季度(q) 可以用 1-2 个占位符, // 年(y)可以用 1-4 个占 ...
- C#获取进程的主窗口句柄的实现方法
通过调用Win32 API实现. public class User32API { private static Hashtable processWnd = null; public delegat ...
- jQuery之防止冒泡事件
冒泡事件就是点击子节点,会向上触发父节点,祖先节点的点击事件. 方法1: event.stopPropagation(); // 阻止事件冒泡 有时候点击提交按钮会有一些默认事件.但是如果没有通过验证 ...