前言

  • 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代码实践,第三节会整理一些更多的思考,再后面还没想好。 本文是系列文并且也会吸取建议进行修改和更新,所以禁止转载。本文欢迎提建议和吐槽。(董铂然博客园)

ReactiveCocoa代码实践之-RAC网络请求重构的更多相关文章

  1. ReactiveCocoa代码实践之-更多思考

    三.ReactiveCocoa代码实践之-更多思考 1. RACObserve()宏形参写法的区别 之前写代码考虑过 RACObserve(self.timeLabel , text) 和 RACOb ...

  2. ReactiveCocoa代码实践之-UI组件的RAC信号操作

    上一节是自己对网络层的一些重构,本节是自己一些代码小实践做出的一些demo程序,基本涵盖大多数UI控件操作. 一.用UISlider实现调色板 假设我们现在做一个demo,上面有一个View用来展示颜 ...

  3. Android网络请求框架

    本篇主要介绍一下Android中经常用到的网络请求框架: 客户端网络请求,就是客户端发起网络请求,经过网络框架的特殊处理,让后将请求发送的服务器,服务器根据 请求的参数,返回客户端需要的数据,经过网络 ...

  4. Xcode7 beta 网络请求报错:The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.

    Xcode7 beta 网络请求报错:The resource could not be loaded because the App Transport Xcode7 beta 网络请求报错:The ...

  5. Xcode7 beta 网络请求报错:The resource could not be loaded because the App Transport

    Xcode7 beta 网络请求报错:The resource could not be loaded because the App Transport Xcode7 beta 网络请求报错:The ...

  6. iOS项目中的网络请求和上下拉刷新封装

    代码地址如下:http://www.demodashi.com/demo/11621.html 一.运行效果图 现在的项目中不可避免的要使用到网络请求,而且几乎所有软件都有上下拉刷新功能,所以我在此对 ...

  7. 多个网络请求成功返回再执行另外任务的思路分析(iOS)

    前言 今天我们来讨论一个经常出现的需求场景,也是一个老话题.在开发中我们往往会遇到需要进行多个网络请求,并且需要多个网络请求成功返回后再做其他事的场景.比如同一个界面显示的内容需要用到两个网络接口,而 ...

  8. css3如何让div一直循环自转圈,附带:网络请求通知图片一直在转圈实例

    css3如何让div一直循环自转圈 代码如下: div{ -webkit-transition-property: -webkit-transform; -webkit-transition-dura ...

  9. WKWebView 网络请求Header 丢失

    WKWebView 是苹果手机上主要的H5加载控件,它相比UIWebView 有诸多优势.在次不做比较,但是它的坑缺比较多.网上也有很多的例子但是做的比较好的真不多,我在这里推荐俩博客供大家参考.ht ...

随机推荐

  1. mysql数据库学习目录

    前面的话 对于前端工程师来说,数据库并不是主要技能点,但是基本的增删改查操作还是需要了解的.小火柴将mysql数据库的学习记录整理如下 目录  前端学数据库之基础操作 前端学数据库之数据类型 前端学数 ...

  2. poj 3414 Pots bfs+模拟

    #include<iostream> #include<cstring> #define fillA 1 #define pourAB 2 #define dropA 3 #d ...

  3. TextView中的部分文字响应点击事件

    TextView是android常用的控件,经常要显示不同文字的大小,颜色,......今天要实现这样这样一个需求,TextView某段内容显示的文字颜色不一样,并且点击区域只能是改变了颜色的字. 1 ...

  4. android给View设置上下左右边框

    给View控件设置边框,可以动态设置上下左右.通过布局文件就能搞定 1.在drawable文件夹下新建一个shape_main_list_bg.xml文件 <layer-list xmlns:a ...

  5. 浅谈2D游戏设计模式3 - 冒险地图之美(1)

    冒险岛之所以能长久的存在,很大一部分原因是因为它的美工设计的非常的精细,以及独特,那么独特以及美究竟体现在哪些方面呢? 今天我就带大家来分析几幅地图吧. 好吧,我们就拿上面这幅美景来分析吧. 1.阳光 ...

  6. HTML5+CSS3实现图片可倾斜摆放的动画相册效果

    先看看效果:其中鼠标悬浮在图片上会有动态效果图 直接上代码: css文件 @CHARSET "UTF-8"; *{ padding:0px; margin:0px; } div{ ...

  7. Android APP压力测试(二)之Monkey信息自动收集脚本

      Android APP压力测试(二) 之Monkey信息自动收集脚本 前言: 上一篇Monkey介绍基本搬抄官方介绍,主要是为了自己查阅方便.本文重点介绍我在进行Monkey时如何自动收集相关信息 ...

  8. WebView JS与RN进行通讯

    RN0.37终于官方增加了WebView与React Native的通讯,之前一真使用的是第三方控件React-Native-WebView-Bridge,但不是知道怎么回事这个第三方控件喊了很长时间 ...

  9. Python_Day_04 set方法总结

    set(集合) 直接创建一个空集合 set_empty = set() print(set_empty) # set() 根据参数创建 # 根据参数 set_argument = set(42,',' ...

  10. 多条asp.net网站的优化建议

    一.返回多个数据集 检查你的访问数据库的代码,看是否存在着要返回多次的请求.每次往返降低了你的应用程序的每秒能够响应请求的次数.通过在单个数据库请求中返回多个结果集,可以减少与数据库通信的时间,使你的 ...