本文簡單介紹了ReactiveCocoa的基礎用法,希望讀完能對這個框架的使用有一個大概的了解旧噪。
前面幾篇文章吨娜,我們研究了ReactiveCocoa(以下簡稱RAC)的起源和思想。網絡上介紹RAC的使用的好文甚多淘钟,在文末的Reference中標出了一些以供參考宦赠。RAC這個開源庫本身也是十分良心,代碼注釋十分齊全米母,因此這一篇文章不做太多的擴展勾扭,僅談一談RAC的基礎使用方法。
RACSignal
上文中說到铁瞒,函數(shù)響應式編程中妙色,將各種通信機制所需要解決的「輸入」與「輸出」的異步關系抽象成了事件/時間驅動的值流,并通過monad
使其支持了函數(shù)式編程的特性慧耍。而在RAC中身辨,這個東西就是RACStream
,開發(fā)過程中我們并不是直接使用它芍碧,而是其子類——RACSignal
和RACSequence
栅表。這一節(jié),講講RACSignal
师枣。
Signal怪瓶,顧名思義,代表一個信號,可以源源不斷地給你傳遞信息洗贰。這樣就好理解RACSignal
代表著「隨時間變化的值流」找岖,這里的值,就包含了將來即將到來的「輸入」敛滋。打個比方许布,一個微博博主便是一個「Signal」,只要沒被封號绎晃,你就會知道將來他會一直發(fā)出消息蜜唾。如果關注了這個博主,一旦他開始發(fā)消息庶艾,新消息會被自動推送到你的設備袁余,因此說RACSignal
是一個Push-Driven
的值流。
那么咱揍,RACSignal
博主會發(fā)出什么消息呢颖榜?一個RACSignal
傳遞的值分為三類:
- Next∶喝梗「Next」代表著一個新的值掩完,一條新的微博。只要這個博主是活躍的硼砰,他就會源源不斷地發(fā)微博且蓬。
- Error√夂玻「Error」則代表著這個Signal出了什么問題缅疟,發(fā)出了一個代表「錯誤」的信號。發(fā)送出「Error」也就意味著這個Signal的消息到此為止了遍愿。比如這位博主被封號了存淫,他就會給你發(fā)一條微博,上面寫著「404Error」沼填,你就知道他再也不會發(fā)微博了……
- Completed桅咆。代表一個Signal完成了自己的全部信息發(fā)送。比如某天這個博主想退出微博了坞笙,于是發(fā)出最后一條微博——「ByeBye粉絲們」岩饼。這就是「Completed」。
一個Signal的信息流薛夜,都是由若干個「Next」籍茧,加上一個代表終結的「Error」或「Completed」組成的。
這些值都是從哪里來的呢梯澜?一個Signal所發(fā)出的信息主要來源有兩種:
- 手動創(chuàng)建一個信號時定義它發(fā)出的信息寞冯。這就好像是一位原創(chuàng)博主,每條微博都是他自己寫的:
// 代碼1
RACSignal *blogSignalA =
[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
[subscriber sendNext:@"blog1"];
[subscriber sendNext:@"blog2"];
[subscriber sendNext:@"blog3"];
[subscriber sendCompleted];
return nil;
}];
- 由其他通信機制生成一個信號時,在其他通信機制產生輸入時發(fā)出消息吮龄。比如某些大V博主的微博就是專門從雜志俭茧、知乎等其他信息載體上將信息搬運過來。RAC提供了很多有力的工具漓帚,讓我們從傳統(tǒng)的Cocoa通信機制中制造出一個信號來:
// 代碼2
// signal from KVO
RACSignal *blogSignalA = RACObserve(someNewspaper, news);
// signal from UIControl events
RACSignal *blogSignalB = [someButton rac_signalForControlEvents:UIControlEventTouchUpInside];
// signal from selectors
RACSignal *blogSignalB = [self rac_signalForSelector:@selector(viewWillAppear:)];
好了母债,現(xiàn)在有一個博主能夠發(fā)出很多消息。但如果沒有人關注他尝抖,這些信息也不會有多大的作用毡们。對于一個Signal也是一樣,創(chuàng)建不是目的昧辽,獲取它發(fā)出的信息才是我們所需要的衙熔。在RAC中,這種行為叫「訂閱」(Subscribe)奴迅。例如,我們想在收到消息時挺据,把消息打印出來取具,或者做一些其他的事情:
// 代碼3
RACSignal *blogSignalA =
[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
[subscriber sendNext:@"blog1"];
[subscriber sendNext:@"blog2"];
[subscriber sendNext:@"blog3"];
[subscriber sendCompleted];
return nil;
}];
//subscribe to blogSignalA
[blogSignalA subscribeNext:^(id _Nullable x) {
NSLog(@"%@",x);
//do something else
} error:^(NSError * _Nullable error) {
NSLog(@"%@",error);
} completed:^{
NSLog(@"Complete!");
}];
現(xiàn)在,我們就關注了blogSignalA
這位博主扁耐,他發(fā)出的blog1
暇检,blog2
等等微博都會推送到我們,由我們進行處理婉称。RAC對于信號的「訂閱者」是有要求的块仆,它必須實現(xiàn)了RACSubscriber
協(xié)議:
// 代碼4
@protocol RACSubscriber <NSObject>
@required
- (void)sendNext:(id)value;
- (void)sendError:(NSError *)error;
- (void)sendCompleted;
- (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable;
@end
這也很好理解,因為「訂閱者」至少得知道自己需要用這些訂閱的值來做什么王暗。上面的代碼3
中的subscribeNext:error:completed:
其實就是幫我們創(chuàng)建了一個內部的「訂閱者」悔据,這些在后續(xù)如果深入探究源碼的時候會詳細說明。
此外俗壹,Signal支持各種函數(shù)式的操作科汗,例如map
,reduce
绷雏,filter
等等头滔。這可以讓我們方便地對原始信號傳輸出的信息進行一步步加工,最終得到我們所需要的值涎显,這就是「函數(shù)性」賦予的利器:
// 代碼5
RACSignal *blogSignalA =
[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
[subscriber sendNext:@"Sunday"];
[subscriber sendNext:@"Monday"];
[subscriber sendNext:@"Tuesday"];
[subscriber sendNext:@"Wednesday"];
[subscriber sendNext:@"Thursday"];
[subscriber sendNext:@"Friday"];
[subscriber sendNext:@"Saturday"];
[subscriber sendCompleted];
return nil;
}];
RACSignal *blogSignalB =
[blogSignalA map:^id _Nullable(NSString * _Nullable value) {
if ([value isEqualToString:@"Sunday"] || [value isEqualToString:@"Saturday"]) {
return @"Weekend";
}else {
return @"Workday";
}
}];
RACSignal *blogSignalC =
[blogSignalB filter:^BOOL(NSString * _Nullable value) {
return [value isEqualToString:@"Weekend"];
}];
[blogSignalC subscribeNext:^(id _Nullable x) {
NSLog(@"Wow Weekend! Time to Relax!");
}];
這里博主A是一個報時的微博坤检,而博主B是一個翻譯的微博,它將A發(fā)出的微博進行加工期吓,然后發(fā)出「Weekend」和「Workday」兩種微博早歇。博主C負責過濾B發(fā)出的微博,屏蔽了所有工作日的消息(Nice)。最后我們關注博主C缺前,就能在收到消息推送的時候知道該出去玩啦蛀醉!當然,有了Monad
的保證衅码,我們也可以采用鏈式語法這么寫:
// 代碼6
RACSignal *blogSignalD =
[[[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
[subscriber sendNext:@"Sunday"];
[subscriber sendNext:@"Monday"];
[subscriber sendNext:@"Tuesday"];
[subscriber sendNext:@"Wednesday"];
[subscriber sendNext:@"Thursday"];
[subscriber sendNext:@"Friday"];
[subscriber sendNext:@"Saturday"];
[subscriber sendCompleted];
return nil;
}] map:^id _Nullable(NSString * _Nullable value) {
if ([value isEqualToString:@"Sunday"] || [value isEqualToString:@"Saturday"]) {
return @"Weekend";
}else {
return @"Workday";
}
}] filter:^BOOL(id _Nullable value) {
return [value isEqualToString:@"Weekend"];
}];
[blogSignalD subscribeNext:^(id _Nullable x) {
NSLog(@"Wow Weekend! Time to Relax!");
}];
總結一下拯刁,RACSignal
的基本操作主要是三個:創(chuàng)建(手動創(chuàng)建+由其他通信機制生成),訂閱逝段,以及轉換垛玻。
RACSubject
上面討論的RACSignal
,其實細究起其行為奶躯,是和微博不太一樣的帚桩。從代碼1
和代碼3
中可以看出,RACSignal
所能發(fā)出的信號是定義好的嘹黔,即創(chuàng)建該Signal
的時候就已經確定了账嚎。這更像是一個「微博機器人」,每當有一個新的粉絲來訂閱它儡蔓,它便按照一個「創(chuàng)建腳本程序」從頭開始生成若干微博進行推送郭蕉。這種行為是依賴于「訂閱」的,只有當「訂閱」發(fā)生的時候喂江,才會對新的訂閱者發(fā)送內容召锈。我們稱之為「冷信號(Cold Signal)」。
這種「Cold Signal」會帶來一個問題获询。譬如說涨岁,這個機器人在它的「創(chuàng)建腳本程序」中進行了其他的操作:
// 代碼7
RACSignal *blogSignalA =
[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
NSLog(@"Ready To Send!");
[subscriber sendNext:@"blog1"];
[subscriber sendNext:@"blog2"];
[subscriber sendNext:@"blog3"];
NSLog(@"Sending Comleted!");
[subscriber sendCompleted];
return nil;
}];
那么,每當有一個新的訂閱者吉嚣,這兩個NSLog
的操作就會重復執(zhí)行一遍梢薪。我們把這種操作稱為「副作用(Side-Effect)」。想象一下如果把上面簡單的NSLog
換成非常復雜的操作尝哆,比如網絡請求沮尿,那么這樣的「副作用」就非常明顯了。因為我們可能只是想進行一次網絡請求较解。RAC中主要使用RACSubject
來解決這個問題畜疾。
RACSubject
是RACSignal
的子類。是不同于只會根據(jù)腳本發(fā)送固定信號的RACSignal
印衔,RACSubject
能夠由我們程序控制啡捶,在任何時候主動發(fā)送新的值。這有點類似于不可變數(shù)組和可變數(shù)組的概念奸焙∠故睿可以想象這樣一種情景彤敛,我們要在原有的舊代碼里利用RAC完成一些功能,那么可以利用RACSubject
了赌,在老代碼中間手動控制其發(fā)送出信號墨榄。因此,官方稱RACSubject
為「most helpful in bridging the non-RAC world to RAC」勿她。
RACSubject
是一個「熱信號」袄秩,它在內部維護了一個「訂閱者」的統(tǒng)計數(shù)組。每當產生新的訂閱行為的時候逢并,它只是簡單地將這個「訂閱者」添加進自己維護的數(shù)組中之剧。等到發(fā)出信號的時候,會遍歷該數(shù)組砍聊,向其中所有的「訂閱者」發(fā)送該信號背稼。也就是說,它不會管有沒有訂閱行為玻蝌,而只是自己發(fā)自己的信號蟹肘。而訂閱之后,也只能收到它以后發(fā)出的信號俯树。嗯帘腹,這樣的行為才是一個活生生的大V博主,而不是冰冷的腳本嘛聘萨!
同時竹椒,RACSubject
還是一個「訂閱者」童太,它實現(xiàn)了RACSubscriber
協(xié)議米辐,也就是說,它可以訂閱一個「RACSignal」书释。當接受到「RACSignal」發(fā)送的信號的時候翘贮,它會遍歷其內部的「訂閱者」數(shù)組,將自己接收到的信號轉發(fā)給每一個「訂閱者」爆惧。也就是說狸页,RACSubject
充當了一個中間轉發(fā)者的角色。這樣既保證了對原始信號只訂閱一次扯再,從而可以消除副作用的影響芍耘,又保證了外界多個訂閱者的正常行為。
<div align=center>
</div>
在RAC中熄阻,這種關系是由RACMulticastConnection
類以及multicast
和connect
操作實現(xiàn)的:
// 代碼8
RACSignal *sideEffectSignal =
[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
NSLog(@"This is side-effect~~~");
[subscriber sendNext:@"1"];
[subscriber sendNext:@"2"];
[subscriber sendNext:@"3"];
[subscriber sendCompleted];
return [RACDisposable disposableWithBlock:^{
}];
}];
RACSubject *subject = [RACSubject subject];
RACMulticastConnection *multiConnect = [sideEffectSignal multicast:subject];
//subscribe A
[multiConnect.signal subscribeNext:^(id _Nullable x) {
NSLog(@"A receive next: %@",x);
} error:^(NSError * _Nullable error) {
NSLog(@"A receive error: %@",error);
} completed:^{
NSLog(@"A receive completed");
}];
//subscribe B
[multiConnect.signal subscribeNext:^(id _Nullable x) {
NSLog(@"B receive next: %@",x);
} error:^(NSError * _Nullable error) {
NSLog(@"B receive error: %@",error);
} completed:^{
NSLog(@"B receive completed");
}];
[multiConnect connect];
/*
Log result:
This is side-effect~~~
A receive next: 1
B receive next: 1
A receive next: 2
B receive next: 2
A receive next: 3
B receive next: 3
A receive completed
B receive completed
*/
RACMulticastConnection
類封裝了原始信號以及充當「中間人」的RACSubject
對象斋竞,調用[multiConnect connect]
會將「中間人」與原始信號連接起來(使用封裝的RACSubject
訂閱原始RACSignal
)。注意這里調用[multiConnect connect]
的時機秃殉,如果將其提前到subscribe A
和subscribe B
之前坝初,那么A和B將完全接收不到原始信號發(fā)出的消息浸剩,這還是因為RACSubject
是一個「熱信號」的原因。如果確實需要先執(zhí)行connect
操作鳄袍,那么在創(chuàng)建RACMulticastConnection
時可以使用RACSubject
的子類绢要,如RACReplaySubject
等來實現(xiàn)具體的需求。
RACSequence
上面說的RACSignal
是一個Push-driven
的值流拗小,而RACSequence
則是一個Pull-driven
的值流重罪,它們的關系就好像是后臺推送和客戶端主動拉取兩種不同行為。
RACSequence
主要用于簡化集合的操作十籍,以及對Cocoa中的基礎集合類型提供函數(shù)性的工具蛆封。譬如說,
// 代碼9
NSArray *names = @[@"Peter",@"John",@"Steve",@"Jim"];
[[names.rac_sequence.signal filter:^BOOL(NSString * _Nullable value) {
return value.length > 4;
}] subscribeNext:^(id _Nullable x) {
NSLog(@"%@",x);
}];
一般情況下勾栗,RACSequence
會采用惰性計算惨篱,即要獲取其中某個元素的時候再去對該元素進行計算。具體思想可以參考臧老師的聊一聊iOS開發(fā)中的惰性計算:
RACCommand
除了上面討論的幾種信號围俘,RAC還為我們提供了很多實用而充滿技巧的工具類砸讳。RACCommand
就是其中一個。顧名思義界牡,它是對一個「操作命令」的封裝:這個操作命令會產生一系列的結果輸出簿寂,而RACCommand
提供了豐富的接口來控制該操作命令的執(zhí)行、取消宿亡,操作的狀態(tài)流等等常遂。
想象一下「人民日報」微博的小編,他掌握著一份程序挽荠,能夠從人民日報官網拉取當天最新的新聞克胳,將這些新聞生成一系列的微博發(fā)出。有了這個程序圈匆,他每天的工作就很輕松了:執(zhí)行一下這個程序就可以了(小編不要打我…)
RACCommand
提供了兩個初始化方法:
- (instancetype)initWithSignalBlock:(RACSignal<ValueType> * (^)(InputType _Nullable input))signalBlock;
- (instancetype)initWithEnabled:(nullable RACSignal<NSNumber *> *)enabledSignal signalBlock:(RACSignal<ValueType> * (^)(InputType _Nullable input))signalBlock;
其中signalBlock
就是上面說的「會產生一系列結果輸出的操作命令」漠另,而enabledSignal
的值則控制了是否能執(zhí)行該操作。RACCommand
提供了excute
接口來執(zhí)行操作命令:
- (RACSignal<ValueType> *)execute:(nullable InputType)input;
成功執(zhí)行操作后跃赚,產生的結果由executionSignals
返回笆搓,每次成功執(zhí)行,都會返回一個RACSignal
纬傲,所以該屬性是一個「高階信號」满败,即「Signal of signal」;倘若執(zhí)行失敗叹括,則會由errors
返回:
@property (nonatomic, strong, readonly) RACSignal<RACSignal<ValueType> *> *executionSignals;
@property (nonatomic, strong, readonly) RACSignal<NSError *> *errors;
RACCommand
還提供了監(jiān)控當前操作狀態(tài)的屬性:
@property (nonatomic, strong, readonly) RACSignal<NSNumber *> *executing;
@property (nonatomic, strong, readonly) RACSignal<NSNumber *> *enabled;
舉個栗子:
// 代碼10
RACSignal *enalbeSignal =
[[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
[subscriber sendNext:@"Sunday"];
[subscriber sendNext:@"Monday"];
[subscriber sendNext:@"Tuesday"];
[subscriber sendNext:@"Wednesday"];
[subscriber sendNext:@"Thursday"];
[subscriber sendNext:@"Friday"];
[subscriber sendNext:@"Saturday"];
[subscriber sendCompleted];
return nil;
}] map:^id _Nullable(NSString * _Nullable value) {
if ([value isEqualToString:@"Sunday"] || [value isEqualToString:@"Saturday"]) {
return @(NO));
}else {
return @(YES));
}
}];
self.command =
[[RACCommand alloc] initWithEnabled:enalbeSignal
signalBlock:^RACSignal * _Nonnull(id _Nullable input) {
return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
[self fetchNewsWithCallback:^(NSError *error, id result) {
if (result) {
[subscriber sendNext:result];
[subscriber sendCompleted];
}
else {
[subscriber sendError:error];
}
}];
return [RACDisposable disposableWithBlock:^{
NSLog(@"send command disposable");
}];
}] ;
}];
// The final signal for blog
RACSignal *blogSignal = [self.command.executionSignals flatten];
上面的例子創(chuàng)建了一個RACCommand
算墨,用來幫助小編同學在工作日從服務器拉取新聞,然后發(fā)送微博领猾。需要注意的是米同,RACCommand
在執(zhí)行操作后骇扇,已將該操作生成的RACSignal
用RACReplaySubject
進行了multicast
,所以不用擔心內部操作所包含的副作用問題面粮。
可以看出少孝,RACCommand
和UIButton
的作用是很相似的:都是用于執(zhí)行某個操作。事實上熬苍,RAC為UIButton
提供了十分方便的category稍走,能生成一個RACCommand
并綁定到button上,使得該button的點擊事件柴底、enable狀態(tài)等等都可以通過這個RACCommand
完成:
self.senderButton.rac_command = self.command;
總結
這里我們用微博的例子來簡單介紹了一下RAC中一些基礎組件的用法婿脸。RAC的功能遠遠不止這幾個基礎組件,甚至遠遠不止是組件所提供的api柄驻。它更代表一種編程風格狐树,一種代碼思想。
當然鸿脓,從這篇文章抑钟,以及自己的實踐也可以看出,RAC還是存在一些缺點:
- RAC對代碼的侵入性很強野哭,如果選擇了使用它在塔,項目代碼將和RAC庫產生很強的耦合。
- RAC不利于團隊協(xié)作拨黔。如果有些團隊成員不熟悉蛔溃,那么將很難調試和修改其他成員用RAC編寫的代碼。
- 調試不友好篱蝇。由于RAC內部操作相當復雜贺待,即使一行簡單的代碼,調試時的堆棧也完全是RAC內部的堆棧信息态兴。也因此狠持,RAC官方更推薦使用
name
或log
進行調試疟位。 - 學習曲線還是比較陡峭的瞻润,需要理解函數(shù)響應式編程的思想,以及學習RAC的基本知識甜刻。調bug的時候甚至還需要充分了解RAC內部原理绍撞。
因此,是否使用RAC到實際的大型項目中得院,這是個見仁見智的話題傻铣。但是一些小項目或是自己學習、研究祥绞、使用非洲,還是十分有價值的鸭限。
Reference
霜神解讀RAC源碼系列
美團點評技術博客的RAC系列
Draveness解讀的RAC源碼系列
ReactiveCocoa and MVVM, an Introduction