ReactiveCocoa框架的使用教程在网上有很多详细的博客可参考,通过学习,我自己也整理了一下,一来便于自己复习,二来分享给大家。先粘贴些优质博文的链接,然后下面以实例的形式一步步讲解。
优质技术博客链接
地址1:ReactiveCocoa入门教程——第一部分
地址2:ReactiveCocoa入门教程——第二部分
地址3:1个小时学会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了,很快.
二、RAC的简单使用----RACSignal
在要使用的RAC的控制器中导入RAC框架的头文件
#import <ReactiveCocoa/ReactiveCocoa.h>
现在来熟悉下RACSignal的使用,从名字就可以看出,它是信号。在viewDidload中加入下面的代码
//创建信号
RACSignal * single = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"想");
[subscriber sendNext:@"发送了信号"];//发送信号
NSLog(@"你");
[subscriber sendCompleted];//发送完成,订阅自动移除
//RACDisposable 可用于手动移除订阅
return [RACDisposable disposableWithBlock:^{
NSLog(@"豆腐");
}];
}];
//订阅信号
NSLog(@"我");
[single subscribeNext:^(id x) {
NSLog(@"吃");
// NSLog(@"信号的值:%@",x);
}];
运行,得到结果如下
这样就可以清楚的看明白,信号的运行流程,但是感觉好乱,下面分析一下:
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<RACSubscriber> subscriber) {
NSLog(@"想");
[subscriber sendNext:@"发送了一个信号"];//发送信号
NSLog(@"你");
//RACDisposable 手动移除订阅者
return [RACDisposable disposableWithBlock:^{
NSLog(@"豆腐");
}];
}];
//订阅信号
NSLog(@"我");
RACDisposable * disposable = [single subscribeNext:^(id x) {
NSLog(@"吃");
NSLog(@"信号的值:%@",x);
}];
//手动移除订阅
[disposable dispose];
打印结果如下
在稍微分析一下,两份代码不同之处是,删去了自动移除订阅[subscriber sendCompleted],添加了手动删除订阅[disposable dispose],手动删除订阅,可以在你想要的地方,合适的时候进行操作。不过手动删除用的少。那既然用得少,我们还是用自动删除吧,优化下,见代码
//创建信号
RACSignal * single = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"想");
[subscriber sendNext:@"发送了信号"];//发送信号
NSLog(@"你");
[subscriber sendCompleted];//发送完成,订阅自动移除
//RACDisposable 手动移除订阅者
return nil;
}];
//订阅信号
NSLog(@"我");
[single subscribeNext:^(id x) {
NSLog(@"吃");
NSLog(@"信号的值:%@",x);
}];
打印结果如下
好了,没豆腐吃了!其实也不需要。返回nil就可以了
上面罗里吧嗦的说了那么多,就是为了理清里面的逻辑,没有结合实际使用,其实听起来还是很迷糊,下面就结合实际,来使用RAC
第二、RAC的常用方法
上面是使用RACSignal创建信号,其实文本框中文字改变也是信号,按钮点击也是信号,RAC为UITxtField和UIButton创建categary,并做好了封装,直接就可以调用它们的信号,这里就围绕着这两个类,进行ARC的使用讲解
在工程中新建一个控制器,添加几个控件,textField,textView,button,label,如下图
订阅textField信号
[self.textfield.rac_textSignal subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
因为textField的信号肯定是NSString,类型的,所以可以写成下面的样子,也更方便使用些
[self.textfield.rac_textSignal subscribeNext:^(NSString* x) {
NSLog(@"%@",x);
}];
这样,当你在文本框输入时,控制台就会打印出输入的内容,如下
可以看到,每次输入都会获取到信号。
filter---信号过滤器
如果我只需要将字符串长度超过3的,才打印,那可以使用过滤器filter,使用方法如下
[[self.textfield.rac_textSignal filter:^BOOL(NSString* value) {
return value.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) {
return value.length>3?YES:NO;
}]map:^id(NSString* value) {
return value.length > 4?[UIColor redColor]:[UIColor whiteColor];
}]subscribeNext:^(UIColor* value) {
self.textfield.backgroundColor = value;
}];
上面的代码的意思是,当输入的字符串长度超过3,就将字符串信号转换成颜色信号,然后订阅该颜色信号,并将颜色赋值给textField的背景色。效果如下
这样的话,当字符串大于3,文本框的背景色变成了红色
另外,RAC提供了一个宏"RAC(对象,属性)"来简化代码并增强可读性,如下
RAC(self.textfield ,backgroundColor) = [self.textfield.rac_textSignal map:^id(NSString* value) {
return value.length > 4?[UIColor redColor]:[UIColor whiteColor];
}];
RAC宏有两个参数,一个是需要设置的对象,一个是设置的属性。这句代码的意思是,当文本框输入的字符串长度大于4时,改变文本框的背景色。这样的话,看起来更清晰,而达到的效果是一样的。
总结一下,到现在为止,学了过滤器:filter,转换器:map,对象设置属性的宏:RAC(要设置的对象,要设置的属性)。可以想象,用这几个方法可以很方便的实现一些功能,比喻说替代通知,监听事件等。
textField的使用是这样,那textView的使用也是这样的,因为他们完全类似
RAC(self.textView ,backgroundColor) = [self.textView.rac_textSignal map:^id(NSString* value) {
return value.length > 4?[UIColor redColor]:[UIColor whiteColor];
}];
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 [NSNumber numberWithBool:([value1 isEqualToString:@"11111"]&&[value2 isEqualToString:@"22222"])];
}];
RAC(self.addButton,enabled) = [mergeTwoSignal map:^id(NSNumber* value) {
return value;
}];
上面的代码的意思是,当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:^(id x) {
//改变label的背景色
self.displayLabel.backgroundColor = [UIColor redColor];
}]
subscribeNext:^(NSNumber * value) {
self.displayLabel.text = @"1314";
}];
这样就实现了,订阅信号前,改变label的背景色
@weakify和@strongify
RAC的所有方法中,大部分是block,所以无法避免在使用过程中导致循环引用,
以前的解决办法是这样的
__weak SecondViewController *bself = self;
[[[self.addButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]
doNext:^(id x) {
//先清掉label中的文字
bself.displayLabel.textColor = [UIColor redColor];
}]
subscribeNext:^(NSNumber * value) {
bself.displayLabel.text = @"1314";
}];
如果每个block都写的话,会很费劲,因为block太多了,还好RAC提供了两个宏,@weakify和@strongify,
@weakify(self);
[[[self.addButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]
doNext:^(id x) {
@strongify(self);
self.displayLabel.textColor = [UIColor redColor];
}]
subscribeNext:^(NSNumber * value) {
@strongify(self);
self.displayLabel.text = @"1314";
}];
@weakify宏让你创建一个弱引用的影子对象(如果你需要多个弱引用,你可以传入多个变量),@strongify让你创建一个对之前传入@weakify对象的强引用。这样就解决了循环引用的问题
第三、RAC在网络请求和图片加载中的使用
先创建一个控制器,添加若干控件,textView,用来展示请求到的数据,imageView,用来展示图片,
使用系统的方法请求数据
在viewDidload中添加下面的代码
NSURL * url = [NSURL URLWithString:urlS];
NSURLSession * session = [NSURLSession sharedSession];
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:url];
NSURLSessionTask * task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSString * dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@",dataString);
NSDictionary * dic = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(@"%@",dic);
[self performSelector:@selector(actionWithString:) onThread:[NSThread mainThread] 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<RACSubscriber> subscriber) {
NSURL * url = [NSURL URLWithString:urlS];
NSURLSession * session = [NSURLSession sharedSession];
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:url];
NSURLSessionTask * task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSString * dataString = [[NSString alloc] 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];
return nil;
}];
}
现在就来调用一下看看,在viewDidLoad中添加下面的代码,
self.requestDataButton是一个按钮,使用方法是点击按钮的时候加载数据
[[[self.requestDataButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]
map:^id(id value) {
return [self racNetworkRequest];
}]
subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
订阅信号后,得到的数据如下
发现,得到的不是想要的数据,而是一个信号对象,其实从racNetworkRequest这个方法中就可以看出,返回就是一个RACSignal对象,如果能获取到RACSignal对象里面的信号流就对了,怎么办呢,RAC提供了这样的方法 flattenMap
flattenMap---获取信号中的信号
把上面的代码写成这样,就可以获取到数据了
[[[self.requestDataButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]
flattenMap:^id(id value) {
return [self racNetworkRequest];
}]
subscribeNext:^(id x) {
NSLog(@"%@",x);
} error:^(NSError *error) {
NSLog(@"%@",error);
}];
这样就会发现订阅到的信号,是你想要的数据了。
then---等待上一个信号的完成,然后订阅自己的信号
[[[self racNetworkRequest]
then:^RACSignal *{
return self.textField.rac_textSignal;
}]
subscribeNext:^(id x) {
NSLog(@"%@",x);
}error:^(NSError *error) {
NSLog(@"error");
}];
then方法会等待前面的信号中completed事件的发送完成,然后再订阅由then block返回的信号。这样就高效地把控制权从一个signal传递给下一个。如此就实现了:当请求数据完成,就可以监控到textField中的文字输入了
回到主线程---deliverOn
因为信号的流转及操作都是在block中完成的,也就是说大部分操作都是在子线程中执行的操作,但是有个时候需要回到主线程完成一些事情,比如,请求到数据后,要刷新UI,这就必须回到主线程,RAC提供了这样的方法deliverOn。用法见下面的代码
@weakify(self)
[[[[[self racNetworkRequest]
then:^RACSignal *{
@strongify(self);
return self.textField.rac_textSignal;
}]
filter:^BOOL(NSString* value) {
return value.length > 3?YES:NO;
}]
deliverOn:[RACScheduler mainThreadScheduler]]//回到主线程
subscribeNext:^(NSString * value) {
@strongify(self);
self.textView.text =value;
NSLog(@"%@",value);
NSLog(@"当前线程%@",[NSThread currentThread]);
} error:^(NSError *error) {
NSLog(@"%@",error);
}];
这样的话,实现的效果就是,在textField中输入文字而且当文字大于3的时候,会在textView中显示出来,而且可以看到订阅信号的block中打印出来的线程是主线程,如下:
悲催的是,如果没有加deliverOn:好像也是在主线程。我也不知道什么原因,不知道有没有用,姑且就认为deliverOn有用,可能在开启很多线程的时候会有用吧
不过我可以在subscribeNext的block中加入回到主线程的方法,也能达到目的,如下
subscribeNext:^(NSString * value) {
@strongify(self);
self.textView.text =value;
NSLog(@"%@",value);
[self performSelectorOnMainThread:@selector(doSomething) withObject:nil waitUntilDone:YES];
NSLog(@"当前线程%@",[NSThread currentThread]);
} error:^(NSError *error) {
NSLog(@"%@",error);
}];
信号节流---throttle
用文本框textField作比喻,当我在里面输入字符时,subscribeNext的block会不停的走,每输入一个字符,就会走一遍。如果我想在输入过程中不需要每改变一个字符就走一遍,而是等输入完成或停止的时候再走block里面的代码,那就可以用throttle,先看效果
[[[self.textField.rac_textSignal
filter:^BOOL(NSString* value) {
return value.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_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
NSURL * url = [NSURL URLWithString:imageUrlString];
UIImage * image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
if (image!=nil) {
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
});
}
});
这样就可以完成图片的加载,看下面的效果
RAC加载图片
创建一个加载图片的方法,方法返回的是RACSignle信号对象,
-(RACSignal*)racRequestImage{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSURL * url = [NSURL URLWithString:imageUrlString];
UIImage * image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
self.imageView.image = image;
[subscriber sendNext:image];
[subscriber sendCompleted];
return nil;
}];
}
下面就来调用这个方法。实现的效果是,点击按钮(self.requestImageDataButton),即开始加载图片,在viewDidLoad中添加下面的代码
@weakify(self);
[[[self.requestImageDataButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]
flattenMap:^RACStream *(id value) {
return [self racRequestImage];
}]
deliverOn:[RACScheduler mainThreadScheduler]]
subscribeNext:^(UIImage * image) {
@strongify(self);
self.imageView.image = image;
}];
运行一下,发现,图片加载正常。从代码量来看,GCD可能方便一点,但是,如果是多个事件凑到一起影响图片加载的时候,RAC或许是不错的选择。
到这一步,就把ReactiveCocoa的初步使用讲完了。
总结一下总共学习哪些方法