http://www.cnblogs.com/manji/p/4846591.html
@H_
301_5@
前言
学习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 respon
SEObject,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);
1 [_
viewmodel.errors subscribeNext:^(NSError *
error) {
2 ResponseData *data =
[ResponseData objectWithKeyValues:error.userInfo];
something error:%@TODO: 这里可以选择一种合适的方式将错误信息展示出来
5 }];
核心点就在于takeUntil,它表示“一直执行直到…”,套用在我们这里就是http请求一直执行,直到cancel命令被下达。经过测试可以发现完全能达到我们的目的。
PS:这里额外介绍下如何模拟不稳定的网络。设置 -> 开发者 -> NETWORK LINK CONDITIONER,里面有各种选项可供选择,比如100% Loss,3G,Very Bad Network等,虽然没有专业工具那么强大,但是简单模拟下异常网络也是足够了。
独自学习RAC还是有一定的难度的,毕竟面对众多RAC的api要想完全理解下来还是挺困难的。而且刚开始不熟悉的情况下很难针对某些特定的场景,想出比较合理的RAC处理方式(这句话是盗用别人的,但是我也深有体会)。