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代码实践,第三节会整理一些更多的思考,再后面还没想好。 本文是系列文并且也会吸取建议进行修改和更新,所以禁止转载。本文欢迎提建议和吐槽。(董铂然博客园)
ReactiveCocoa代码实践之-RAC网络请求重构的更多相关文章
- ReactiveCocoa代码实践之-更多思考
三.ReactiveCocoa代码实践之-更多思考 1. RACObserve()宏形参写法的区别 之前写代码考虑过 RACObserve(self.timeLabel , text) 和 RACOb ...
- ReactiveCocoa代码实践之-UI组件的RAC信号操作
上一节是自己对网络层的一些重构,本节是自己一些代码小实践做出的一些demo程序,基本涵盖大多数UI控件操作. 一.用UISlider实现调色板 假设我们现在做一个demo,上面有一个View用来展示颜 ...
- Android网络请求框架
本篇主要介绍一下Android中经常用到的网络请求框架: 客户端网络请求,就是客户端发起网络请求,经过网络框架的特殊处理,让后将请求发送的服务器,服务器根据 请求的参数,返回客户端需要的数据,经过网络 ...
- 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 ...
- 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 ...
- iOS项目中的网络请求和上下拉刷新封装
代码地址如下:http://www.demodashi.com/demo/11621.html 一.运行效果图 现在的项目中不可避免的要使用到网络请求,而且几乎所有软件都有上下拉刷新功能,所以我在此对 ...
- 多个网络请求成功返回再执行另外任务的思路分析(iOS)
前言 今天我们来讨论一个经常出现的需求场景,也是一个老话题.在开发中我们往往会遇到需要进行多个网络请求,并且需要多个网络请求成功返回后再做其他事的场景.比如同一个界面显示的内容需要用到两个网络接口,而 ...
- css3如何让div一直循环自转圈,附带:网络请求通知图片一直在转圈实例
css3如何让div一直循环自转圈 代码如下: div{ -webkit-transition-property: -webkit-transform; -webkit-transition-dura ...
- WKWebView 网络请求Header 丢失
WKWebView 是苹果手机上主要的H5加载控件,它相比UIWebView 有诸多优势.在次不做比较,但是它的坑缺比较多.网上也有很多的例子但是做的比较好的真不多,我在这里推荐俩博客供大家参考.ht ...
随机推荐
- OpenCascade B-Spline Basis Function
OpenCascade B-Spline Basis Function eryar@163.com Abstract. B-splines are quite a bit more flexible ...
- SQL Server 存储过程生成insert语句
你肯定有过这样的烦恼,同样的表,不同的数据库,加入你不能执行select insert 那么你肯定需要一条这样的存储过程,之需要传入表明,就会给你生成数据的插入语句. 当然数据表数量太大,你将最好用 ...
- 基于Bootstrap里面的Button dropdown打造自定义select
最近工作非常的忙,在对一个系统进行改版.项目后台是MVC1.0开发的,但是前端部分已经改过几个版本,而已之前的设计师很强大,又做设计又做前端开发.而已很时尚和前沿,使用了一直都很热门的Bootstra ...
- Python学习第一弹——Python环境搭建
一.Python简介: Python,是一种面向对象.解释型计算机程序设计语言,由Guido van Rossum于1989年底发明,第一个公开发行版发行于1991年.Python语法简洁而清晰,具有 ...
- java 中多线程之间的通讯之等待唤醒机制
wait notify () nitifyAll () 都使用在同步中,因为要对持有监视器(锁)的线程操作 所以要使用在同步中,因为只有同步才具有锁 为什么这些操作线程的方法要定义object类中呢 ...
- Freemark笔记
Freemark基本语法知识 Freemark 常用代码总结1 Freemark 常用代码总结2 笔记,吐槽一下freemark的蛋疼语法. 1.elseif 中间不能有空格 2.三目运算符 语法和j ...
- Josephus环问题
约瑟夫环问题 问题描述: Josephus问题可以描述为如下的一个游戏:N个人编号从1到N,围坐成一个圆圈,从1号开始传递一个热土豆,经过M次传递后拿着土豆的人离开圈子,由坐在离开的人的后面的人拿起热 ...
- 【JUC】JDK1.8源码分析之Semaphore(六)
一.前言 分析了CountDownLatch源码后,下面接着分析Semaphore的源码.Semaphore称为计数信号量,它允许n个任务同时访问某个资源,可以将信号量看做是在向外分发使用资源的许可证 ...
- OpenGL新手框架
开始学习用OpenGL,也就想显示一些点,以为挺简单的,哎,看了两天才会画三维的点,做个总结. 使用OpenGL的基本流程 int main(int argv, char *argc[]) { //初 ...
- using语法糖详解 2015-01-06 17:45 50人阅读 评论(0) 收藏
前段事件在using外套try catch 突然想到,如果出现异常 会不会执行释放,不执行的话那服务器很可能导致崩溃... 特意上了CSDN问了大神..得到了答案.. Using相等于try catc ...