ReactiveCocoa框架的使用教程在網(wǎng)上有很多詳細(xì)的博客可參考,通過學(xué)習(xí),我自己也整理了一下,一來便于自己復(fù)習(xí),二來分享給大家。先粘貼些優(yōu)質(zhì)博文的鏈接,然后下面以實(shí)例的形式一步步講解。
優(yōu)質(zhì)技術(shù)博客鏈接
地址1:ReactiveCocoa入門教程——第一部分
地址2:ReactiveCocoa入門教程——第二部分
地址3:1個(gè)小時(shí)學(xué)會(huì)ReactiveCocoa基本使用
下面開始介紹ReactiveCocoa的使用
一、在項(xiàng)目中集成ReactiveCocoa框架
既然是第三方框架,那用CocoaPods集成是最方便的。
首先,創(chuàng)建一個(gè)工程
然后,在工程中創(chuàng)建Podfile文件,文件中的內(nèi)容如下:
platform :ios,'9.0'
use_frameworks!
target 'RWReactivePlayground' do
pod 'ReactiveCocoa', '~> 2.5'
end
注意1:
集成ReactiveCocoa框架和其他的不同之處是多了一個(gè)“use_frameworks!”,我在使用過程中發(fā)現(xiàn),2.5版本以上的更高的版本要加上“use_frameworks!”,否則會(huì)報(bào)錯(cuò),導(dǎo)致集成不了。而2.5版本之前的(包括2.5版本),就不需要加“use_frameworks!”,
注意2:
ReactiveCocoa現(xiàn)在的最高版本已經(jīng)到5.0了,問題是,如果用swift編程,那么集成最新版本的ReactiveCocoa框架沒有問題,但是如果使用OC編程的話,那最高只能集成2.5版本的RAC(RAC是ReactiveCocoa的簡稱),否則集成好了以后工程會(huì)報(bào)錯(cuò)。
簡單的說就是,如果你用swift編程,用Cocoapods集成時(shí),Podfile文件這么寫
platform :ios,'9.0'
use_frameworks!
target 'RWReactivePlayground' do
pod 'ReactiveCocoa', '~> 5.0'
end
如果你用的是oc編程,用Cocoapods集成時(shí),Podfile文件這么寫
platform :ios,'9.0'
target 'RWReactivePlayground' do
pod 'ReactiveCocoa', '~> 2.5'
end
最后,上面的工作都做好了,就可以集成RAC了,很快.
二、RAC的簡單使用----RACSignal
在要使用的RAC的控制器中導(dǎo)入RAC框架的頭文件
#import <ReactiveCocoa/ReactiveCocoa.h>
現(xiàn)在來熟悉下RACSignal的使用,從名字就可以看出,它是信號。在viewDidload中加入下面的代碼
//創(chuàng)建信號
RACSignal * single = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"想");
[subscriber sendNext:@"發(fā)送了信號"];//發(fā)送信號
NSLog(@"你");
[subscriber sendCompleted];//發(fā)送完成,訂閱自動(dòng)移除
//RACDisposable 可用于手動(dòng)移除訂閱
return [RACDisposable disposableWithBlock:^{
NSLog(@"豆腐");
}];
}];
//訂閱信號
NSLog(@"我");
[single subscribeNext:^(id x) {
NSLog(@"吃");
// NSLog(@"信號的值:%@",x);
}];
運(yùn)行,得到結(jié)果如下
這樣就可以清楚的看明白,信號的運(yùn)行流程,但是感覺好亂,下面分析一下:
1.createSignal方法 是創(chuàng)建信號,創(chuàng)建好的信號,沒有被訂閱前,只是冷信號,此時(shí)是不會(huì)走createSignal后面的block的。
程序往下,就走到“NSLog(@"我")”,
2.然后走到subscribeNext,這一步就是訂閱信號,訂閱號信號后,信號single就變成了熱信號,
3.既然變成熱信號,就開始走createSignal后面的block中的去,所以就打印出了“NSLog(@"想")”。
4.下面是sendNext,即發(fā)送信號,發(fā)送了信號,訂閱者就會(huì)收到信號,發(fā)送的內(nèi)容可以從訂閱信號subscribeNext后面的block中獲取到,程序就走到subscribeNext后面的block中,所以就打印了“NSLog(@"吃")”,
5.當(dāng)訂閱信號的subscribeNext后面的block走完以后,程序又回到,createSignal后面的block中,繼續(xù)未完成的代碼,所以就打印“NSLog(@"你")”,繼續(xù)往下就是[subscriber sendCompleted],這句代碼的意思是,發(fā)送完成了,訂閱自動(dòng)移除,沒有了訂閱者了,信號又變成了冷信號。
6.接下來就是return,返回一個(gè)RACDisposable對象,這個(gè)的作用就是,可以用來手動(dòng)移除訂閱。RACDisposable對象,創(chuàng)建完成,就走進(jìn)創(chuàng)建方法的block中,也就是打印NSLog(@"豆腐")
綜上,打印出來的結(jié)果就是“我想吃你豆腐”,它就是這樣出來的
這里再介紹下RACDisposable的使用,將代碼改一下
//創(chuàng)建信號
RACSignal * single = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"想");
[subscriber sendNext:@"發(fā)送了一個(gè)信號"];//發(fā)送信號
NSLog(@"你");
//RACDisposable 手動(dòng)移除訂閱者
return [RACDisposable disposableWithBlock:^{
NSLog(@"豆腐");
}];
}];
//訂閱信號
NSLog(@"我");
RACDisposable * disposable = [single subscribeNext:^(id x) {
NSLog(@"吃");
NSLog(@"信號的值:%@",x);
}];
//手動(dòng)移除訂閱
[disposable dispose];
打印結(jié)果如下
在稍微分析一下,兩份代碼不同之處是,刪去了自動(dòng)移除訂閱[subscriber sendCompleted],添加了手動(dòng)刪除訂閱[disposable dispose],手動(dòng)刪除訂閱,可以在你想要的地方,合適的時(shí)候進(jìn)行操作。不過手動(dòng)刪除用的少。那既然用得少,我們還是用自動(dòng)刪除吧,優(yōu)化下,見代碼
//創(chuàng)建信號
RACSignal * single = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"想");
[subscriber sendNext:@"發(fā)送了信號"];//發(fā)送信號
NSLog(@"你");
[subscriber sendCompleted];//發(fā)送完成,訂閱自動(dòng)移除
//RACDisposable 手動(dòng)移除訂閱者
return nil;
}];
//訂閱信號
NSLog(@"我");
[single subscribeNext:^(id x) {
NSLog(@"吃");
NSLog(@"信號的值:%@",x);
}];
打印結(jié)果如下
好了,沒豆腐吃了!其實(shí)也不需要。返回nil就可以了
上面羅里吧嗦的說了那么多,就是為了理清里面的邏輯,沒有結(jié)合實(shí)際使用,其實(shí)聽起來還是很迷糊,下面就結(jié)合實(shí)際,來使用RAC
第二、RAC的常用方法
上面是使用RACSignal創(chuàng)建信號,其實(shí)文本框中文字改變也是信號,按鈕點(diǎn)擊也是信號,RAC為UITxtField和UIButton創(chuàng)建categary,并做好了封裝,直接就可以調(diào)用它們的信號,這里就圍繞著這兩個(gè)類,進(jìn)行ARC的使用講解
在工程中新建一個(gè)控制器,添加幾個(gè)控件,textField,textView,button,label,如下圖
訂閱textField信號
[self.textfield.rac_textSignal subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
因?yàn)閠extField的信號肯定是NSString,類型的,所以可以寫成下面的樣子,也更方便使用些
[self.textfield.rac_textSignal subscribeNext:^(NSString* x) {
NSLog(@"%@",x);
}];
這樣,當(dāng)你在文本框輸入時(shí),控制臺就會(huì)打印出輸入的內(nèi)容,如下
可以看到,每次輸入都會(huì)獲取到信號。
filter---信號過濾器
如果我只需要將字符串長度超過3的,才打印,那可以使用過濾器filter,使用方法如下
[[self.textfield.rac_textSignal filter:^BOOL(NSString* value) {
return value.length>3?YES:NO;
}]subscribeNext:^(NSString* x) {
NSLog(@"過濾后的到的信號:%@",x);
}];
在文本框中輸入字符,打印結(jié)果如下
可以看到,只有當(dāng)字符串長度大于3的信號,才會(huì)被訂閱到
map--轉(zhuǎn)換器
map就是將一種信號轉(zhuǎn)換成你想要的另一種信號,這里把字符串信號,轉(zhuǎn)換成文字信號
如果想當(dāng)文本框中輸入的文字長度大于4的時(shí)候,改變文本框的背景色,一種方法是把過濾器的條件設(shè)置為4,然后在subscribeNext的block中直接給textField.backgroundColor賦值。不過RAC有轉(zhuǎn)換信號的方法---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;
}];
上面的代碼的意思是,當(dāng)輸入的字符串長度超過3,就將字符串信號轉(zhuǎn)換成顏色信號,然后訂閱該顏色信號,并將顏色賦值給textField的背景色。效果如下
這樣的話,當(dāng)字符串大于3,文本框的背景色變成了紅色
另外,RAC提供了一個(gè)宏"RAC(對象,屬性)"來簡化代碼并增強(qiáng)可讀性,如下
RAC(self.textfield ,backgroundColor) = [self.textfield.rac_textSignal map:^id(NSString* value) {
return value.length > 4?[UIColor redColor]:[UIColor whiteColor];
}];
RAC宏有兩個(gè)參數(shù),一個(gè)是需要設(shè)置的對象,一個(gè)是設(shè)置的屬性。這句代碼的意思是,當(dāng)文本框輸入的字符串長度大于4時(shí),改變文本框的背景色。這樣的話,看起來更清晰,而達(dá)到的效果是一樣的。
總結(jié)一下,到現(xiàn)在為止,學(xué)了過濾器:filter,轉(zhuǎn)換器:map,對象設(shè)置屬性的宏:RAC(要設(shè)置的對象,要設(shè)置的屬性)。可以想象,用這幾個(gè)方法可以很方便的實(shí)現(xiàn)一些功能,比喻說替代通知,監(jiān)聽事件等。
textField的使用是這樣,那textView的使用也是這樣的,因?yàn)樗麄兺耆愃?/p>
RAC(self.textView ,backgroundColor) = [self.textView.rac_textSignal map:^id(NSString* value) {
return value.length > 4?[UIColor redColor]:[UIColor whiteColor];
}];
combineLatest:reduce:
想象一下,如果當(dāng)textField和textView同時(shí)滿足某個(gè)條件時(shí),才能進(jìn)行某項(xiàng)操作的話,應(yīng)該如何寫呢?RAC為我們準(zhǔn)備了一個(gè)方法--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;
}];
上面的代碼的意思是,當(dāng)textField中的文字為"11111",同時(shí)textView中的文字為"22222"的時(shí)候,返回一個(gè)信號,信號的類型是NSNumber,然后通過轉(zhuǎn)換器map,將值返回,返回的值用于確定按鈕是否可用。
可能會(huì)疑問,map中返回的NSNumber類型的,而button的enabled屬性是BOOL類型,怎么可以這樣直接賦值,但是RAC它就是可以,就是做的這么好。
到這一步,就可以訂閱button的點(diǎn)擊信號了,看代碼就懂了
[[self.addButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]
subscribeNext:^(NSNumber * value) {
//標(biāo)簽賦值
self.displayLabel.text = @"1314";
}];
運(yùn)行驗(yàn)證一下,結(jié)果如下
確實(shí)能達(dá)到要求。真好,再也不用給button 添加點(diǎn)擊事件了。
doNext
現(xiàn)在講一下附加操作doNext,它的作用是,在不改變信號的基礎(chǔ)上,進(jìn)行一些附加的操作,比喻說,我在訂閱到給label賦值前,改變label的背景色,當(dāng)然也可以是做別的操作。反正是附加的不會(huì)影響信號流的。使用見代碼
[[[self.addButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]
doNext:^(id x) {
//改變label的背景色
self.displayLabel.backgroundColor = [UIColor redColor];
}]
subscribeNext:^(NSNumber * value) {
self.displayLabel.text = @"1314";
}];
這樣就實(shí)現(xiàn)了,訂閱信號前,改變label的背景色
@weakify和@strongify
RAC的所有方法中,大部分是block,所以無法避免在使用過程中導(dǎo)致循環(huán)引用,
以前的解決辦法是這樣的
__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";
}];
如果每個(gè)block都寫的話,會(huì)很費(fèi)勁,因?yàn)閎lock太多了,還好RAC提供了兩個(gè)宏,@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宏讓你創(chuàng)建一個(gè)弱引用的影子對象(如果你需要多個(gè)弱引用,你可以傳入多個(gè)變量),@strongify讓你創(chuàng)建一個(gè)對之前傳入@weakify對象的強(qiáng)引用。這樣就解決了循環(huán)引用的問題
第三、RAC在網(wǎng)絡(luò)請求和圖片加載中的使用
先創(chuàng)建一個(gè)控制器,添加若干控件,textView,用來展示請求到的數(shù)據(jù),imageView,用來展示圖片,
使用系統(tǒng)的方法請求數(shù)據(jù)
在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;
}
結(jié)果如下
使用RAC請求網(wǎng)絡(luò)數(shù)據(jù)
把系統(tǒng)請求網(wǎng)絡(luò)數(shù)據(jù)的方法,封裝成信號流
//rac網(wǎng)絡(luò)請求
-(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];//發(fā)送信號
[subscriber sendCompleted];//結(jié)束發(fā)送
} else {
[subscriber sendError:error];//發(fā)送錯(cuò)誤
}
}];
[task resume];
return nil;
}];
}
現(xiàn)在就來調(diào)用一下看看,在viewDidLoad中添加下面的代碼,
self.requestDataButton是一個(gè)按鈕,使用方法是點(diǎn)擊按鈕的時(shí)候加載數(shù)據(jù)
[[[self.requestDataButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]
map:^id(id value) {
return [self racNetworkRequest];
}]
subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
訂閱信號后,得到的數(shù)據(jù)如下
發(fā)現(xiàn),得到的不是想要的數(shù)據(jù),而是一個(gè)信號對象,其實(shí)從racNetworkRequest這個(gè)方法中就可以看出,返回就是一個(gè)RACSignal對象,如果能獲取到RACSignal對象里面的信號流就對了,怎么辦呢,RAC提供了這樣的方法 flattenMap
flattenMap---獲取信號中的信號
把上面的代碼寫成這樣,就可以獲取到數(shù)據(jù)了
[[[self.requestDataButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]
flattenMap:^id(id value) {
return [self racNetworkRequest];
}]
subscribeNext:^(id x) {
NSLog(@"%@",x);
} error:^(NSError *error) {
NSLog(@"%@",error);
}];
這樣就會(huì)發(fā)現(xiàn)訂閱到的信號,是你想要的數(shù)據(jù)了。
then---等待上一個(gè)信號的完成,然后訂閱自己的信號
[[[self racNetworkRequest]
then:^RACSignal *{
return self.textField.rac_textSignal;
}]
subscribeNext:^(id x) {
NSLog(@"%@",x);
}error:^(NSError *error) {
NSLog(@"error");
}];
then方法會(huì)等待前面的信號中completed事件的發(fā)送完成,然后再訂閱由then block返回的信號。這樣就高效地把控制權(quán)從一個(gè)signal傳遞給下一個(gè)。如此就實(shí)現(xiàn)了:當(dāng)請求數(shù)據(jù)完成,就可以監(jiān)控到textField中的文字輸入了
回到主線程---deliverOn
因?yàn)樾盘柕牧鬓D(zhuǎn)及操作都是在block中完成的,也就是說大部分操作都是在子線程中執(zhí)行的操作,但是有個(gè)時(shí)候需要回到主線程完成一些事情,比如,請求到數(shù)據(jù)后,要刷新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(@"當(dāng)前線程%@",[NSThread currentThread]);
} error:^(NSError *error) {
NSLog(@"%@",error);
}];
這樣的話,實(shí)現(xiàn)的效果就是,在textField中輸入文字而且當(dāng)文字大于3的時(shí)候,會(huì)在textView中顯示出來,而且可以看到訂閱信號的block中打印出來的線程是主線程,如下:
悲催的是,如果沒有加deliverOn:好像也是在主線程。我也不知道什么原因,不知道有沒有用,姑且就認(rèn)為deliverOn有用,可能在開啟很多線程的時(shí)候會(huì)有用吧
不過我可以在subscribeNext的block中加入回到主線程的方法,也能達(dá)到目的,如下
subscribeNext:^(NSString * value) {
@strongify(self);
self.textView.text =value;
NSLog(@"%@",value);
[self performSelectorOnMainThread:@selector(doSomething) withObject:nil waitUntilDone:YES];
NSLog(@"當(dāng)前線程%@",[NSThread currentThread]);
} error:^(NSError *error) {
NSLog(@"%@",error);
}];
信號節(jié)流---throttle
用文本框textField作比喻,當(dāng)我在里面輸入字符時(shí),subscribeNext的block會(huì)不停的走,每輸入一個(gè)字符,就會(huì)走一遍。如果我想在輸入過程中不需要每改變一個(gè)字符就走一遍,而是等輸入完成或停止的時(shí)候再走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);
}];
這樣得到的效果是,當(dāng)輸入字符串長度大于3,而且該字符串的值在1s內(nèi)沒有改變,就把textField中的值,賦值給textView。所以簡單的說throttle的作用:如果前面信號在設(shè)定的時(shí)間內(nèi)沒有變化時(shí),throttle就會(huì)把信號傳到下面的事件中去。
使用系統(tǒng)的方法加載圖片
系統(tǒng)的方法,我就不說了 看代碼
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加載圖片
創(chuàng)建一個(gè)加載圖片的方法,方法返回的是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;
}];
}
下面就來調(diào)用這個(gè)方法。實(shí)現(xiàn)的效果是,點(diǎn)擊按鈕(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;
}];
運(yùn)行一下,發(fā)現(xiàn),圖片加載正常。從代碼量來看,GCD可能方便一點(diǎn),但是,如果是多個(gè)事件湊到一起影響圖片加載的時(shí)候,RAC或許是不錯(cuò)的選擇。
到這一步,就把ReactiveCocoa的初步使用講完了。
總結(jié)一下總共學(xué)習(xí)哪些方法