iOS ReactiveCocoa框架的简单使用

写在前面:本文章非本人原创,原创地址为:https://www.jianshu.com/p/148075efc2c9

ReactiveCocoa框架的使用教程在网上有很多详细的博客可参考,通过学习,我自己也整理了一下,一来便于自己复习,二来分享给大家。先粘贴些优质博文的链接,然后下面以实例的形式一步步讲解。

下面开始介绍ReactiveCocoa的使用

一、在项目中集成ReactiveCocoa框架

既然是第三方框架,那用CocoaPods集成是最方便的。

首先,创建一个工程

然后,在工程中创建Podfile文件,文件中的内容如下:

platform :ios,'9.0'

use_frameworks!

target 'RWReactivePlayground' do

pod 'ReactiveCocoa', '~> 2.5'

end

注意1:

集成ReactiveCocoa框架和其他的不同之处是多了一个“use_frameworks!”,我在使用过程中发现,2.5版本以上的更高的版本要加上“use_frameworks!”,否则会报错,导致集成不了。而2.5版本之前的(包括2.5版本),就不需要加“use_frameworks!”,

注意2:

ReactiveCocoa现在的最高版本已经到5.0了,问题是,如果用swift编程,那么集成最新版本的ReactiveCocoa框架没有问题,但是如果使用OC编程的话,那最高只能集成2.5版本的RAC(RAC是ReactiveCocoa的简称),否则集成好了以后工程会报错。

简单的说就是,如果你用swift编程,用Cocoapods集成时,Podfile文件这么写

platform :ios,'9.0'

use_frameworks!

target 'RWReactivePlayground' do

pod 'ReactiveCocoa', '~> 5.0'

end

如果你用的是oc编程,用Cocoapods集成时,Podfile文件这么写

platform :ios,'9.0'

target 'RWReactivePlayground' do

pod 'ReactiveCocoa', '~> 2.5'

end

最后,上面的工作都做好了,就可以集成RAC了,很快.

屏幕快照 2017-02-27 上午10.59.13.png

二、RAC的简单使用----RACSignal

在要使用的RAC的控制器中导入RAC框架的头文件

#import<ReactiveCocoa/ReactiveCocoa.h>

现在来熟悉下RACSignal的使用,从名字就可以看出,它是信号。在viewDidload中加入下面的代码

//创建信号RACSignal * single = [RACSignal createSignal:^RACDisposable *(id subscriber) {NSLog(@"想");        [subscriber sendNext:@"发送了信号"];//发送信号NSLog(@"你");        [subscriber sendCompleted];//发送完成,订阅自动移除//RACDisposable 可用于手动移除订阅return[RACDisposable disposableWithBlock:^{NSLog(@"豆腐");        }];    }];//订阅信号NSLog(@"我");    [single subscribeNext:^(idx) {NSLog(@"吃");//        NSLog(@"信号的值:%@",x);}];

运行,得到结果如下

RACSinale.png

这样就可以清楚的看明白,信号的运行流程,但是感觉好乱,下面分析一下:

1.createSignal方法 是创建信号,创建好的信号,没有被订阅前,只是冷信号,此时是不会走createSignal后面的block的。

程序往下,就走到“NSLog(@"我")”,

2.然后走到subscribeNext,这一步就是订阅信号,订阅号信号后,信号single就变成了热信号,

3.既然变成热信号,就开始走createSignal后面的block中的去,所以就打印出了“NSLog(@"想")”。

4.下面是sendNext,即发送信号,发送了信号,订阅者就会收到信号,发送的内容可以从订阅信号subscribeNext后面的block中获取到,程序就走到subscribeNext后面的block中,所以就打印了“NSLog(@"吃")”,

5.当订阅信号的subscribeNext后面的block走完以后,程序又回到,createSignal后面的block中,继续未完成的代码,所以就打印“NSLog(@"你")”,继续往下就是[subscriber sendCompleted],这句代码的意思是,发送完成了,订阅自动移除,没有了订阅者了,信号又变成了冷信号。

6.接下来就是return,返回一个RACDisposable对象,这个的作用就是,可以用来手动移除订阅。RACDisposable对象,创建完成,就走进创建方法的block中,也就是打印NSLog(@"豆腐")

综上,打印出来的结果就是“我想吃你豆腐”,它就是这样出来的

这里再介绍下RACDisposable的使用,将代码改一下

//创建信号RACSignal * single = [RACSignal createSignal:^RACDisposable *(id subscriber) {NSLog(@"想");        [subscriber sendNext:@"发送了一个信号"];//发送信号NSLog(@"你");//RACDisposable 手动移除订阅者return[RACDisposable disposableWithBlock:^{NSLog(@"豆腐");        }];    }];//订阅信号NSLog(@"我");    RACDisposable * disposable = [single subscribeNext:^(idx) {NSLog(@"吃");NSLog(@"信号的值:%@",x);    }];//手动移除订阅[disposable dispose];

打印结果如下

在稍微分析一下,两份代码不同之处是,删去了自动移除订阅[subscriber sendCompleted],添加了手动删除订阅[disposable dispose],手动删除订阅,可以在你想要的地方,合适的时候进行操作。不过手动删除用的少。那既然用得少,我们还是用自动删除吧,优化下,见代码

//创建信号RACSignal * single = [RACSignal createSignal:^RACDisposable *(id subscriber) {NSLog(@"想");        [subscriber sendNext:@"发送了信号"];//发送信号NSLog(@"你");        [subscriber sendCompleted];//发送完成,订阅自动移除//RACDisposable 手动移除订阅者returnnil;    }];//订阅信号NSLog(@"我");    [single subscribeNext:^(idx) {NSLog(@"吃");NSLog(@"信号的值:%@",x);    }];

打印结果如下

好了,没豆腐吃了!其实也不需要。返回nil就可以了

上面罗里吧嗦的说了那么多,就是为了理清里面的逻辑,没有结合实际使用,其实听起来还是很迷糊,下面就结合实际,来使用RAC

第二、RAC的常用方法

上面是使用RACSignal创建信号,其实文本框中文字改变也是信号,按钮点击也是信号,RAC为UITxtField和UIButton创建categary,并做好了封装,直接就可以调用它们的信号,这里就围绕着这两个类,进行ARC的使用讲解

在工程中新建一个控制器,添加几个控件,textField,textView,button,label,如下图

订阅textField信号

[self.textfield.rac_textSignal subscribeNext:^(idx) {NSLog(@"%@",x);    }];

因为textField的信号肯定是NSString,类型的,所以可以写成下面的样子,也更方便使用些

[self.textfield.rac_textSignal subscribeNext:^(NSString* x) {NSLog(@"%@",x);    }];

这样,当你在文本框输入时,控制台就会打印出输入的内容,如下

可以看到,每次输入都会获取到信号。

filter---信号过滤器

如果我只需要将字符串长度超过3的,才打印,那可以使用过滤器filter,使用方法如下

[[self.textfield.rac_textSignal filter:^BOOL(NSString* value) {returnvalue.length>3?YES:NO;    }]subscribeNext:^(NSString* x) {NSLog(@"过滤后的到的信号:%@",x);    }];

在文本框中输入字符,打印结果如下

可以看到,只有当字符串长度大于3的信号,才会被订阅到

map--转换器

map就是将一种信号转换成你想要的另一种信号,这里把字符串信号,转换成文字信号

如果想当文本框中输入的文字长度大于4的时候,改变文本框的背景色,一种方法是把过滤器的条件设置为4,然后在subscribeNext的block中直接给textField.backgroundColor赋值。不过RAC有转换信号的方法---map,如下

[[[self.textfield.rac_textSignal filter:^BOOL(NSString* value) {returnvalue.length>3?YES:NO;    }]map:^id(NSString* value) {returnvalue.length >4?[UIColorredColor]:[UIColorwhiteColor];    }]subscribeNext:^(UIColor* value) {self.textfield.backgroundColor = value;    }];

上面的代码的意思是,当输入的字符串长度超过3,就将字符串信号转换成颜色信号,然后订阅该颜色信号,并将颜色赋值给textField的背景色。效果如下

这样的话,当字符串大于3,文本框的背景色变成了红色

另外,RAC提供了一个宏"RAC(对象,属性)"来简化代码并增强可读性,如下

RAC(self.textfield ,backgroundColor) = [self.textfield.rac_textSignal map:^id(NSString* value) {returnvalue.length >4?[UIColorredColor]:[UIColorwhiteColor];    }];

RAC宏有两个参数,一个是需要设置的对象,一个是设置的属性。这句代码的意思是,当文本框输入的字符串长度大于4时,改变文本框的背景色。这样的话,看起来更清晰,而达到的效果是一样的。

总结一下,到现在为止,学了过滤器:filter,转换器:map,对象设置属性的宏:RAC(要设置的对象,要设置的属性)。可以想象,用这几个方法可以很方便的实现一些功能,比喻说替代通知,监听事件等。

textField的使用是这样,那textView的使用也是这样的,因为他们完全类似

RAC(self.textView ,backgroundColor) = [self.textView.rac_textSignal map:^id(NSString* value) {returnvalue.length >4?[UIColorredColor]:[UIColorwhiteColor];    }];

combineLatest:reduce:

想象一下,如果当textField和textView同时满足某个条件时,才能进行某项操作的话,应该如何写呢?RAC为我们准备了一个方法--combineLatest:reduce:信号合并

先看代码

RACSignal * mergeTwoSignal = [RACSignal combineLatest:@[self.textfield.rac_textSignal,self.textView.rac_textSignal] reduce:^id(NSString* value1,NSString* value2){return[NSNumbernumberWithBool:([value1 isEqualToString:@"11111"]&&[value2 isEqualToString:@"22222"])];    }];RAC(self.addButton,enabled) = [mergeTwoSignal map:^id(NSNumber* value) {returnvalue;    }];

上面的代码的意思是,当textField中的文字为"11111",同时textView中的文字为"22222"的时候,返回一个信号,信号的类型是NSNumber,然后通过转换器map,将值返回,返回的值用于确定按钮是否可用。

可能会疑问,map中返回的NSNumber类型的,而button的enabled属性是BOOL类型,怎么可以这样直接赋值,但是RAC它就是可以,就是做的这么好。

到这一步,就可以订阅button的点击信号了,看代码就懂了

[[self.addButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]    subscribeNext:^(NSNumber* value) {//标签赋值self.displayLabel.text =@"1314";    }];

运行验证一下,结果如下

确实能达到要求。真好,再也不用给button 添加点击事件了。

doNext

现在讲一下附加操作doNext,它的作用是,在不改变信号的基础上,进行一些附加的操作,比喻说,我在订阅到给label赋值前,改变label的背景色,当然也可以是做别的操作。反正是附加的不会影响信号流的。使用见代码

[[[self.addButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]    doNext:^(idx) {//改变label的背景色self.displayLabel.backgroundColor = [UIColorredColor];    }]    subscribeNext:^(NSNumber* value) {self.displayLabel.text =@"1314";    }];

这样就实现了,订阅信号前,改变label的背景色

@weakify和@strongify

RAC的所有方法中,大部分是block,所以无法避免在使用过程中导致循环引用,

以前的解决办法是这样的

__weakSecondViewController *bself =self;    [[[self.addButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]        doNext:^(idx) {//先清掉label中的文字bself.displayLabel.textColor = [UIColorredColor];        }]      subscribeNext:^(NSNumber* value) {          bself.displayLabel.text =@"1314";      }];

如果每个block都写的话,会很费劲,因为block太多了,还好RAC提供了两个宏,@weakify和@strongify,

@weakify(self);    [[[self.addButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]      doNext:^(idx) {          @strongify(self);self.displayLabel.textColor = [UIColorredColor];      }]    subscribeNext:^(NSNumber* value) {        @strongify(self);self.displayLabel.text =@"1314";    }];

@weakify宏让你创建一个弱引用的影子对象(如果你需要多个弱引用,你可以传入多个变量),@strongify让你创建一个对之前传入@weakify对象的强引用。这样就解决了循环引用的问题

第三、RAC在网络请求和图片加载中的使用

先创建一个控制器,添加若干控件,textView,用来展示请求到的数据,imageView,用来展示图片,

使用系统的方法请求数据

在viewDidload中添加下面的代码

NSURL* url = [NSURLURLWithString:urlS];NSURLSession* session = [NSURLSessionsharedSession];NSMutableURLRequest* request = [NSMutableURLRequestrequestWithURL:url];NSURLSessionTask* task = [session dataTaskWithRequest:request completionHandler:^(NSData* _Nullable data,NSURLResponse* _Nullable response,NSError* _Nullable error) {NSString* dataString = [[NSStringalloc] initWithData:data encoding:NSUTF8StringEncoding];NSLog(@"%@",dataString);NSDictionary* dic = [NSJSONSerializationJSONObjectWithData:data options:0error:nil];NSLog(@"%@",dic);        [selfperformSelector:@selector(actionWithString:) onThread:[NSThreadmainThread] withObject:dataString waitUntilDone:YES];    }];    [task resume];

//回到主线程给textView赋值-(void)actionWithString:(id)value{self.textView.text = (NSString*)value;}

结果如下

使用RAC请求网络数据

把系统请求网络数据的方法,封装成信号流

//rac网络请求-(RACSignal *)racNetworkRequest{return[RACSignal createSignal:^RACDisposable *(id subscriber) {NSURL* url = [NSURLURLWithString:urlS];NSURLSession* session = [NSURLSessionsharedSession];NSMutableURLRequest* request = [NSMutableURLRequestrequestWithURL:url];NSURLSessionTask* task = [session dataTaskWithRequest:request completionHandler:^(NSData* _Nullable data,NSURLResponse* _Nullable response,NSError* _Nullable error) {NSString* dataString = [[NSStringalloc] initWithData:data encoding:NSUTF8StringEncoding];//            NSLog(@"%@",dataString);//            NSDictionary * dic = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];//            NSLog(@"%@",dic);if(error ==nil) {//返回成功[subscriber sendNext:dataString];//发送信号[subscriber sendCompleted];//结束发送}else{                [subscriber sendError:error];//发送错误}        }];        [task resume];returnnil;    }];}

现在就来调用一下看看,在viewDidLoad中添加下面的代码,

self.requestDataButton是一个按钮,使用方法是点击按钮的时候加载数据

[[[self.requestDataButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]    map:^id(idvalue) {return[selfracNetworkRequest];    }]    subscribeNext:^(idx) {NSLog(@"%@",x);    }];

订阅信号后,得到的数据如下

发现,得到的不是想要的数据,而是一个信号对象,其实从racNetworkRequest这个方法中就可以看出,返回就是一个RACSignal对象,如果能获取到RACSignal对象里面的信号流就对了,怎么办呢,RAC提供了这样的方法 flattenMap

flattenMap---获取信号中的信号

把上面的代码写成这样,就可以获取到数据了

[[[self.requestDataButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]      flattenMap:^id(idvalue) {return[selfracNetworkRequest];      }]    subscribeNext:^(idx) {NSLog(@"%@",x);    } error:^(NSError*error) {NSLog(@"%@",error);    }];

这样就会发现订阅到的信号,是你想要的数据了。

then---等待上一个信号的完成,然后订阅自己的信号

[[[selfracNetworkRequest]    then:^RACSignal *{returnself.textField.rac_textSignal;    }]    subscribeNext:^(idx) {NSLog(@"%@",x);    }error:^(NSError*error) {NSLog(@"error");    }];

then方法会等待前面的信号中completed事件的发送完成,然后再订阅由then block返回的信号。这样就高效地把控制权从一个signal传递给下一个。如此就实现了:当请求数据完成,就可以监控到textField中的文字输入了

回到主线程---deliverOn

因为信号的流转及操作都是在block中完成的,也就是说大部分操作都是在子线程中执行的操作,但是有个时候需要回到主线程完成一些事情,比如,请求到数据后,要刷新UI,这就必须回到主线程,RAC提供了这样的方法deliverOn。用法见下面的代码

@weakify(self)    [[[[[selfracNetworkRequest]        then:^RACSignal *{            @strongify(self);returnself.textField.rac_textSignal;        }]      filter:^BOOL(NSString* value) {returnvalue.length >3?YES:NO;      }]    deliverOn:[RACScheduler mainThreadScheduler]]//回到主线程subscribeNext:^(NSString* value) {        @strongify(self);self.textView.text =value;NSLog(@"%@",value);NSLog(@"当前线程%@",[NSThreadcurrentThread]);    } error:^(NSError*error) {NSLog(@"%@",error);    }];

这样的话,实现的效果就是,在textField中输入文字而且当文字大于3的时候,会在textView中显示出来,而且可以看到订阅信号的block中打印出来的线程是主线程,如下:

悲催的是,如果没有加deliverOn:好像也是在主线程。我也不知道什么原因,不知道有没有用,姑且就认为deliverOn有用,可能在开启很多线程的时候会有用吧

不过我可以在subscribeNext的block中加入回到主线程的方法,也能达到目的,如下

subscribeNext:^(NSString* value) {        @strongify(self);self.textView.text =value;NSLog(@"%@",value);        [selfperformSelectorOnMainThread:@selector(doSomething) withObject:nilwaitUntilDone:YES];NSLog(@"当前线程%@",[NSThreadcurrentThread]);    } error:^(NSError*error) {NSLog(@"%@",error);    }];

信号节流---throttle

用文本框textField作比喻,当我在里面输入字符时,subscribeNext的block会不停的走,每输入一个字符,就会走一遍。如果我想在输入过程中不需要每改变一个字符就走一遍,而是等输入完成或停止的时候再走block里面的代码,那就可以用throttle,先看效果

[[[self.textField.rac_textSignal      filter:^BOOL(NSString* value) {returnvalue.length >3?YES:NO;      }]      throttle:1]    subscribeNext:^(NSString* value) {        @strongify(self);self.textView.text =value;    } error:^(NSError*error) {NSLog(@"%@",error);    }];

这样得到的效果是,当输入字符串长度大于3,而且该字符串的值在1s内没有改变,就把textField中的值,赋值给textView。所以简单的说throttle的作用:如果前面信号在设定的时间内没有变化时,throttle就会把信号传到下面的事件中去。

使用系统的方法加载图片

系统的方法,我就不说了 看代码

dispatch_queue_tqueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);dispatch_async(queue, ^{NSURL* url = [NSURLURLWithString:imageUrlString];UIImage* image = [UIImageimageWithData:[NSDatadataWithContentsOfURL:url]];if(image!=nil) {dispatch_async(dispatch_get_main_queue(), ^{self.imageView.image = image;            });        }    });

这样就可以完成图片的加载,看下面的效果

RAC加载图片

创建一个加载图片的方法,方法返回的是RACSignle信号对象,

-(RACSignal*)racRequestImage{return[RACSignal createSignal:^RACDisposable *(id subscriber) {NSURL* url = [NSURLURLWithString:imageUrlString];UIImage* image = [UIImageimageWithData:[NSDatadataWithContentsOfURL:url]];self.imageView.image = image;        [subscriber sendNext:image];        [subscriber sendCompleted];returnnil;    }];}

下面就来调用这个方法。实现的效果是,点击按钮(self.requestImageDataButton),即开始加载图片,在viewDidLoad中添加下面的代码

@weakify(self);    [[[self.requestImageDataButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]      flattenMap:^RACStream *(idvalue) {return[selfracRequestImage];      }]    deliverOn:[RACScheduler mainThreadScheduler]]    subscribeNext:^(UIImage* image) {        @strongify(self);self.imageView.image = image;    }];

运行一下,发现,图片加载正常。从代码量来看,GCD可能方便一点,但是,如果是多个事件凑到一起影响图片加载的时候,RAC或许是不错的选择。

到这一步,就把ReactiveCocoa的初步使用讲完了。

总结一下总共学习哪些方法

filter---信号过滤器

map--转换器

combineLatest:reduce:--信号合并

doNext--附加操作

@weakify和@strongify--避免循环

flattenMap---获取信号中的信号

then---等待上一个信号的完成,然后订阅自己的信号

回到主线程---deliverOn

信号节流---throttle

作者:爬树的蚂蚁

链接:https://www.jianshu.com/p/148075efc2c9

來源:简书

简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容