ReactiveCocoa代码实践之-RAC网络请求重构
前言
- RAC相比以往的开发模式主要有以下优点:提供了统一的消息传递机制;提供了多种奇妙且高效的信号操作方法;配合MVVM设计模式和RAC宏绑定减少多端依赖。
- RAC的理论知识非常深厚,包含有FRP,高阶函数,冷信号与热信号,RAC Operation,信号的生命周期等,这些文档里都有介绍。 但是由于RAC本身的特性,可能会听上去容易上手难。
- 本文还是从一个比较接地气的角度开始的。因为现在要做一个完美100%的全项目ReactiveCocoa架构基本不太现实,大多数项目都会有很多历史包袱,我们只能渐渐的向RAC靠拢,将一段段恶心的代码重构,使逻辑功能更加清晰。
本节主要我之前对网络请求的重构的一个简单记录。
一.普通请求重构
旧代码结构图:
之前的代码控制器中都是一个个需要连接网络的方法中直接调用service的请求方法并获取回调,属于常规做法。
// controller.m ************************************ // 控制器中的某一处方法 - (void)requestForTop{ [MDSBezelActivityView activityViewForView:self.view withLabel:@"加载中..."]; // 直接调用service里的请求方法 [SXFeedbackService requestForFeedbackSummarySuccess:^(NSDictionary *result) { [MDSBezelActivityView removeView]; // 成功后相关处理 } failure:^(AFHTTPRequestOperation *operation, NSError *error) { [MDSBezelActivityView removeView]; // 失败后相关处理 }]; }
重构后结构图:
使用RAC改写后,controller不会直接调用service,controller通过控制一个个command的执行与否来达到发请求的目的。得到数据后绑定的值一旦发生改变,会来到RACObserve的回调方法。并且如果请求失败,也会以错误信号的方式传递到execute的subscribeError回调方法里。 executing可以用来监听命令是否执行完。
// controller.m ************************************ @property(nonatomic,strong)SXFeedbackMainViewModel *viewModel; - (void)viewDidLoad{ [self addRACObserve]; } // 在页面初次加载时设置绑定 - (void)addRACObserve{ @weakify(self); [[RACObserve(self.viewModel, topNumEntity) skip:1] subscribeNext:^(id x) { @strongify(self); // 绑定viewModel的值一旦改变来到这里。 }]; } // 原本用来发请求的地方 - (void)requestForTop { [[self.viewModel.fetchFeedbackSummaryCommand execute:nil] subscribeError:^(NSError *error) { // 对错误的处理 }]; [[self.viewModel.fetchFeedbackSummaryCommand.executing skip:1] subscribeNext:^(NSNumber *executing) { if ([executing boolValue]) { [MDSBezelActivityView activityViewForView:self.view withLabel:@"加载中..."]; }else{ [MDSBezelActivityView removeView]; } }]; } // viewModel.m ************************************ - (instancetype)init { self = [super init]; [self setupRACCommand]; return self; } // 初始化设定一个指令用来打开某个请求 - (void) setupRACCommand { @weakify(self); _fetchFeedbackSummaryCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) { return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { // 这里面更彻底的方法是直接将请求写成一个operation,但是大多数项目的网络层应该都有manager或是签名等原因想直接改成那种结构可能比较复杂 ,所以这里面的代码像是RAC和直接请求的结合。 [SXMerchantAutorityService requestForFeedbackSummarySuccess:^(NSDictionary *result) { @strongify(self); // 成功回调后做的相关操作 [subscriber sendCompleted]; } failure:^(AFHTTPRequestOperation *operation, NSError *error) { [subscriber sendError:error]; }]; return nil; }]; }]; }
二.需要传参数的请求
上面是普通的请求,就是请求地址是写死或者是从全局变量中拼接参数的。 如果需要传入若干参数的话controller无法直接接触到service,所以需要以viewModel作为媒介传值,有两种传值方法。
1.通过viewModel的属性
这种方法可用于参数少,一个或两个的。直接在viewModel里加上一些属性,然后controller在适当的时候给这个属性赋值。 在viewModel中的RACCommand中调用service方法需要参数时直接从自己的属性取。
// controller.m ************************************ self.viewModel.isAccess = self.isAccess; [self requestForTop]; // viewModel.h ************************************ // input参数 /** * 是美团还是点评 */ @property(nonatomic, assign) BOOL isAccess; // viewModel.m ************************************ _fetchFeedbackSummaryCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) { return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { [SXMerchantAutorityService requestForFeedbackSummaryWithType:self.isAccess success:^(NSDictionary *result) { // 成功 } failure:^(AFHTTPRequestOperation *operation, NSError *error) { // 失败 }]; return nil; }]; }];
如果是用RAC宏设置viewModel和controller的某些属性绑定,那也可以省去手动给viewModel的set方法赋值这一步。(董铂然博客园)
2.通过execute方法参数传值
这种方法适用于参数较多的情况无法一一列为viewModel的属性。 这时候建议设置一个对象模型,然后在execute方法前将这个模型建立好并赋值,然后作为参数传入。
比如这种常见的列表类的具有多个参数的请求方法:
// service.h ************************************ /** * 获取评价列表 */ + (void)requestForFeedbacklistWithSource:(BOOL)isFromWeb dealid:(NSInteger)dealid poiid:(NSInteger)poiid labelName:(NSString *)labelName type:(NSString *)type readStatus:(NSString *)readStatus replyStatus:(NSString *)replyStatus limit:(NSNumber *)limit offset:(NSNumber *)offset success:(void(^)(NSDictionary *result))success failure:(void(^)(AFHTTPRequestOperation *operation, NSError *error))failure;
在controller的发请求方法中旧方法就是直接调用service的请求接口,这里不再列出,下面列出RAC的写法。
// controller.m ************************************ - (void)requestForDataWithType:(int)type { // ------给RACComand传入一个input模型。 SXFeedbackListRequestModel *input = [SXFeedbackListRequestModel new]; input.replyStatus = self.replyStatus; // 这里也可以写成一个工厂方法 input.readStatus = self.readStatus; input.isMeituan = self.isMeituan; input.dealid = self.dealid; input.poiid = self.poiid; input.type = self.type; input.labelName = labelName; input.offset = @(self.offset); input.limit = @(10); // 上面的input在这里作为参数传入 [[self.viewModel.fetchFeedbackListCommand execute:input] subscribeNext:^(id x) { // ------这里处理正确的操作。 } error:^(NSError *error) { // ------这里处理失败的操作。 }]; } // viewModel.m ************************************ - (void) setupRACCommand { _fetchFeedbackListCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(SXFeedbackListRequestModel *input) { return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { // 用前面execute传入的参数会传到这个地方 [SXMerchantAutorityService requestForFeedbacklistWithSource:input.isFormWeb dealid:input.dealid poiid:input.poiid labelName:input.labelName type:input.type readStatus:input.readStatus replyStatus:input.replyStatus limit:input.limit offset:input.offset success:^(NSDictionary *result) { @strongify(self); // 一些操作 [subscriber sendCompleted]; } failure:^(AFHTTPRequestOperation *operation, NSError *error) { [subscriber sendError:error]; }]; return nil; }]; }]; }
可能会觉得在这个command中要把之前的模型的每一个属性都扒出来传到参数里行为有点冗余。 可以将之前service里的那个参数很多的方法改写成只需要传入一个模型。然后command这里就可以直接传入模型了,反正在方法内部再取出来也不麻烦。我这边考虑到了其他非RAC地方的兼容性就没有改了。
三.所有请求完成才消除toast
这里是一个类似于请求combo的概念。所有的请求全部结束后才消除加载中的progressHUD ,如果在普通的架构下可用dispatch调度组来解决,但是RAC实现这个功能非常简单,主要方法是通过executing信号来判断一个命令的的状态,然后使用combineLatest操作来监听多个command的状态,combineLatest操作的特征是监听的多个信号只要有一个改变了就把所有信号组成一个tuple返回。
// 监听executing RACSignal *hud = [RACSignal combineLatest:@[self.viewModel.fetchFeedbackListCommand.executing,self.viewModel.fetchFeedbackSummaryCommand.executing]]; [hud subscribeNext:^(RACTuple *x) { if (![x.first boolValue]&&![x.second boolValue]) { [MDSBezelActivityView removeView]; }else{ [MDSBezelActivityView activityViewForView:self.view withLabel:@"加载中..."]; } }];
这个建议和之前RACObserve写在一起。 也可以改成filter的写法。
// 可以把加载HUD的代码写在最前面,然后后面直接控制消除HUD [[hud filter:^BOOL(RACTuple *x) { return ![x.first boolValue]&&![x.second boolValue]; }] subscribeNext:^(id x) { [MDSBezelActivityView removeView]; }];
还有另一种方法也可以实现这种需求,rac_liftSelector这个方法是只有所有数组中的信号都发出sendNext信号时才会调用那个@selector的方法,并且这个方法的三个参数分别就是那三个sendNext发的。 所有的都回来了再统一打包,这主要适用于三个请求都是异步没有依赖关系。
@weakify(self); [[self rac_liftSelector:@selector(doWithA:withB:withC) withSignalsFromArray:@[signalA,signalB,signalC]] subscribeError:^(NSError *error) { @strongify(self); [MDSBezelActivityView removeView]; } completed:^{ [MDSBezelActivityView removeView]; }];
combineLatest和liftselector两种combo的方法有一定的区别,具体的使用可以结合需求。前者是每一个请求回来了都会回调一下,后者是全部回来了再调用方法。(董铂然博客园)
四.结果数据的传递
如果是希望所有的请求都完成了所有数据都获得了,后再刷新界面,使用上面统一消除toast的方法时同样适合的。 把消除toast那行代码改成[self.tableVIew reloadData]或其他代码即可。
因为现在的主流是希望能够瘦身Controller, 所以一般也建议将业务逻辑、判断、计算、拼接字符串放在viewModel里,最后直接把需要的数据返回,控制器只负责得到干脆的数据后直接展示界面。 下面的例子是一个文本标签上文字的获得方法
// Controller.m ************************************ // ViewDidLoad RAC(self.replyCountLabel,text) = RACObserve(self.viewModel, replyCountLabelTitle); // ViewModel.m ************************************ _fetchNewsDetailCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) { return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { @strongify(self); [self requestForNewsDetailSuccess:^(NSDictionary *result) { // 这边省去一些判空代码 self.detailModel = [SXNewsDetailEntity detailWithDict:result[self.newsModel.docid]]; // 中间还有一些其他的操作省略 NSInteger count = [self.newsModel.replyCount intValue]; // 这里是直接把拼接好的标题返回,现实中还会遇到更复杂的逻辑 if ([self.newsModel.replyCount intValue] > 10000) { self.replyCountBtnTitle = [NSString stringWithFormat:@"%.1f万跟帖",count/10000.0]; }else{ self.replyCountBtnTitle = [NSString stringWithFormat:@"%ld跟帖",count]; } [subscriber sendCompleted]; } failure:^(AFHTTPRequestOperation *operation, NSError *error) { [subscriber sendError:error]; }]; return nil; }]; }];
重构时可以将更多控制器的属性比如模型,或数组,放到viewModel里。 以前控制器里的self.replyModels 改成self.ViewModel.replyModels。
// ViewModel.h ************************************ /** * 相似新闻 */ @property(nonatomic,strong)NSArray *similarNews; /** * 搜索关键字 */ @property(nonatomic,strong)NSArray *keywordSearch; /** * 获取搜索结果数组命令 */ @property(nonatomic, strong) RACCommand *fetchNewsDetailCommand; // ViewModel.m ************************************ // 某个command里调用发请求方法成功的回调内 self.similarNews = [SXSimilarNewsEntity objectArrayWithKeyValuesArray:result[self.newsModel.docid][@"relative_sys"]]; self.keywordSearch = result[self.newsModel.docid][@"keyword_search"]; [subscriber sendCompleted]; // Controller.m ************************************ // 随便拿了个方法举例 - (CGFloat )tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { switch (section) { case 0: return self.webView.height; break; case 1: return self.viewModel.replyModels.count > 0 ? 40 : CGFLOAT_MIN; break; case 2: return self.viewModel.similarNews.count > 0 ? 40 : CGFLOAT_MIN; break; default: return CGFLOAT_MIN; break; } }
合理的分离之后应该是Controller只有一些UI控件,ViewModel中存放模型属性,命令,和一些业务逻辑操作或判断的方法等。
对其中的一些demo代码感兴趣的可以fork下这里的代码 https://github.com/dsxNiubility/SXNews 。以前是用土方法写了个小项目,现在旧代码移到了old分支,master分支上持续在做一些RAC相关的改动。
参照如上所说的方法进行重构,controller的代码将会大大的减少,业务逻辑也会更加明朗。后续的第二节会整理一些特殊UI组件的RAC代码实践,第三节会整理一些更多的思考,再后面还没想好。 本文是系列文并且也会吸取建议进行修改和更新,所以禁止转载。本文欢迎提建议和吐槽。(董铂然博客园)