前言
本文是根據DeveloperLx在鬥魚直播ReactiveCocoa
時整理的文章,同時加上本人的見解,若有錯誤希望指出。
感謝 DeveloperLx 為大家做出的貢獻,雖然直播聲音小了點,但滿滿的都是乾貨 ^v^
簡介
神馬是RAC?ReactiveCocoa(簡稱為RAC),是由Github開源的一個應用於iOS和OS開發的新框架,Cocoa是蘋果整套框架的簡稱,因此很多蘋果框架喜歡以Cocoa結尾。借用RayWenderlich上面的話:
As an iOS developer, nearly every line of code you write is in reaction to some event; a button tap, a received network message, a property change (via Key Value Observing) or a change in user’s location via CoreLocation are all good examples. However, these events are all encoded in different ways; as actions, delegates, KVO, callbacks and others. ReactiveCocoa defines a standard interface for events, so they can be more easily chained, filtered and composed using a basic set of tools.
翻譯過來就是:
作為一個iOS開發者,你寫的每一行程式碼幾乎都是在響應某個事件,例如按鈕的點選,收到網路訊息,屬性的變化(通過KVO)或者使用者位置的變化(通過CoreLocation)。但是這些事件都用不同的方式來處理,比如action、delegate、KVO、callback等。ReactiveCocoa為事件定義了一個標準介面,從而可以使用一些基本工具來更容易的連線、過濾和組合。
RAC是由 Mattt Thompson
大神開發的,很多開發者對其的評價是開啟一個新Objective-C紀元
,可見對其評價有多高。
以下是RAC的Github主頁:ReactiveCocoa
以及官方給出的用法連結
安裝
ReactiveCocoa安裝教程我就不說了,用pod安裝即可。
本文的Demo可在文章最後下載,在閱讀本文的時候,強烈推薦邊看Demo邊看博文。
專案中加入了ReactiveCocoa 和 DeveloperLx 大神的列印外掛 LxDBAnything。
同時加入了鍵盤相應的第三方IQKeyboardManager,然而沒有怎麼用到。
以及Masonry,使用方法可以看這篇文章,iOS – Masonry自動佈局(Autolayout)。
擼程式碼
第一部分 簡單使用
文字框事件
原來我們在使用textFiled
的時候我們需要寫到
1 |
[textField addTarget:self action:@selector(textChanged:) forControlEvents:UIControlEventEditingChanged]; |
然後實現textChanged:
方法,在RAC中,對於文字框的監聽,是非常簡單的一件事情,看如下程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
UITextField * textField = ({ UITextField * textField = [[UITextField alloc]init]; textField.backgroundColor = [UIColor cyanColor]; textField; }); [self.view addSubview:textField]; @weakify(self); // __weak __typeof__(self) self_weak_ = self; [textField mas_makeConstraints:^(MASConstraintMaker *make) { @strongify(self); // __strong __typeof__(self) self = self_weak_; make.size.mas_equalTo(CGSizeMake(180, 40)); make.center.equalTo(self.view); }]; [[textField rac_signalForControlEvents:UIControlEventEditingChanged] subscribeNext:^(id x) { LxDBAnyVar(x); }]; [textField.rac_textSignal subscribeNext:^(NSString *x) { LxDBAnyVar(x); }]; |
列印結果:
1 2 3 4 5 6 7 8 9 |
?__31-[ViewController textFiledTest]_block_invoke_2 + 215? x = 12 ?__31-[ViewController textFiledTest]_block_invoke241 + 211? x = 0x7fe810c51a90; frame = (97.5 313.5; 180 40); text = '123'; clipsToBounds = YES; opaque = NO; gestureRecognizers = 0x7fe810f58fb0>; layer = 0x7fe810c51600>> ?__31-[ViewController textFiledTest]_block_invoke_2 + 215? x = 123 ?__31-[ViewController textFiledTest]_block_invoke241 + 211? x = 0x7fe810c51a90; frame = (97.5 313.5; 180 40); text = '1231'; clipsToBounds = YES; opaque = NO; gestureRecognizers = 0x7fe810f58fb0>; layer = 0x7fe810c51600>> ?__31-[ViewController textFiledTest]_block_invoke_2 + 215? x = 1231 ?__31-[ViewController textFiledTest]_block_invoke241 + 211? x = 0x7fe810c51a90; frame = (97.5 313.5; 180 40); text = '12312'; clipsToBounds = YES; opaque = NO; gestureRecognizers = 0x7fe810f58fb0>; layer = 0x7fe810c51600>> ?__31-[ViewController textFiledTest]_block_invoke_2 + 215? x = 12312 ?__31-[ViewController textFiledTest]_block_invoke241 + 211? x = 0x7fe810c51a90; frame = (97.5 313.5; 180 40); text = '123123'; clipsToBounds = YES; opaque = NO; gestureRecognizers = 0x7fe810f58fb0>; layer = 0x7fe810c51600>> ?__31-[ViewController textFiledTest]_block_invoke_2 + 215? x = 123123 |
我們很容易的監聽到textFiled
中發生的變化,其中x的型別預設為id型別, 我們已知它的型別的時候我們可以將其改變,就像上面程式碼,將id改成了NSString型別。
手勢
1 2 3 4 5 6 7 |
self.view.userInteractionEnabled = YES; UITapGestureRecognizer * tap = [[UITapGestureRecognizer alloc]init]; [[tap rac_gestureSignal] subscribeNext:^(UITapGestureRecognizer * tap) { LxDBAnyVar(tap); }]; [self.view addGestureRecognizer:tap]; |
為了方便,我們直接新增到self.view上,點選螢幕,得到列印結果:
1 |
?__29-[ViewController gestureTest]_block_invoke + 184? tap = 0x7fa2e3e1f9f0; state = Ended; view = 0x7fa2e3e20b70>; target= action=sendNext:, target=0x7fa2e3c064f0>)>> |
通知
1 2 3 4 |
[[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil] subscribeNext:^(NSNotification * notification) { LxDBAnyVar(notification); }]; |
我們建立了一個通知,叫做進入後臺, 當程式進入後臺的時候通知相應,當我們用RAC寫通知的時候,我們有一個好處,就是不用removeObserver通知,因為RAC通知的監聽者師RAC自己,它會幫你管理釋放方法。可以看方法實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
- (RACSignal *)rac_addObserverForName:(NSString *)notificationName object:(id)object { @unsafeify(object); return [[RACSignal createSignal:^(idRACSubscriber> subscriber) { @strongify(object); id observer = [self addObserverForName:notificationName object:object queue:nil usingBlock:^(NSNotification *note) { [subscriber sendNext:note]; }]; return [RACDisposable disposableWithBlock:^{ [self removeObserver:observer]; }]; }] setNameWithFormat:@"-rac_addObserverForName: %@ object: ", notificationName, [object class], object]; } |
定時器
1 2 3 4 5 6 7 8 9 10 11 |
//1. 延遲某個時間後再做某件事 [[RACScheduler mainThreadScheduler]afterDelay:2 schedule:^{ LxPrintAnything(rac); }]; //2. 每間隔多長時間做一件事 [[RACSignal interval:1 onScheduler:[RACScheduler mainThreadScheduler]]subscribeNext:^(NSDate * date) { LxDBAnyVar(date); }]; |
這是定時器最常用的兩種寫法,第一種方法,延遲時間去做某件事,更改afterDelay
的屬性。
第二種方法,每間隔多長時間做一件事,更改interval
屬性。
代理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
UIAlertView * alertView = [[UIAlertView alloc]initWithTitle:@"RAC" message:@"ReactiveCocoa" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Ensure", nil]; [[self rac_signalForSelector:@selector(alertView:clickedButtonAtIndex:) fromProtocol:@protocol(UIAlertViewDelegate)] subscribeNext:^(RACTuple * tuple) { LxDBAnyVar(tuple); LxDBAnyVar(tuple.first); LxDBAnyVar(tuple.second); LxDBAnyVar(tuple.third); }]; [alertView show]; // 更簡單的方式: [[alertView rac_buttonClickedSignal]subscribeNext:^(id x) { LxDBAnyVar(x); }]; |
用RAC去寫代理的時候,會有侷限,只能取代沒有返回值的代理方法,什麼是沒有返回值的代理呢?比如說tableView的代理方法:
1 2 3 4 |
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath |
這兩個方法一個返回的是CGFloat
,一個是void
,RAC只能取代void的代理。
KVO
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
UIScrollView * scrollView = [[UIScrollView alloc]init]; scrollView.delegate = (id)self; [self.view addSubview:scrollView]; UIView * scrollViewContentView = [[UIView alloc]init]; scrollViewContentView.backgroundColor = [UIColor yellowColor]; [scrollView addSubview:scrollViewContentView]; @weakify(self); [scrollView mas_makeConstraints:^(MASConstraintMaker *make) { @strongify(self); make.edges.equalTo(self.view).insets(UIEdgeInsetsMake(80, 80, 80, 80)); }]; [scrollViewContentView mas_makeConstraints:^(MASConstraintMaker *make) { @strongify(self); make.edges.equalTo(scrollView); make.size.mas_equalTo(CGSizeMake(CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame))); }]; [RACObserve(scrollView, contentOffset) subscribeNext:^(id x) { LxDBAnyVar(x); }]; |
用RAC寫KVO的好處就是方法簡單,keypath有程式碼提示。
第二部分 進階
訊號
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
- (RACSignal *)loginSignal { return [RACSignal createSignal:^RACDisposable *(idsubscriber) { RACDisposable * schedulerDisposable = [[RACScheduler mainThreadScheduler]afterDelay:2 schedule:^{ if (arc4random()%10 > 1) { [subscriber sendNext:@"Login response"]; [subscriber sendCompleted]; } else { [subscriber sendError:[NSError errorWithDomain:@"LOGIN_ERROR_DOMAIN" code:444 userInfo:@{}]]; } }]; return [RACDisposable disposableWithBlock:^{ [schedulerDisposable dispose]; }]; }]; } |
RAC的核心就是RACSignal,也就是訊號,我們可以直接建立訊號createSignal
,併傳送它sendNext
,當訊號完成後我們同時用dispose
方法銷燬它。傳送訊號,我們同時也要訂閱訊號,訂閱訊號程式碼如下:
1 2 3 4 5 6 7 8 9 10 |
[signal subscribeNext:^(id x) { LxDBAnyVar(x); } error:^(NSError *error) { LxDBAnyVar(error); } completed:^{ LxPrintAnything(completed); }]; |
在訊號傳送的時候, 錯誤的時候,以及完成的時候,我們都可以得到相應。
訊號的處理
map (對映)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
UITextField * textField = ({ UITextField * textField = [[UITextField alloc]init]; textField.backgroundColor = [UIColor cyanColor]; textField; }); [self.view addSubview:textField]; @weakify(self); // __weak __typeof__(self) self_weak_ = self; [textField mas_makeConstraints:^(MASConstraintMaker *make) { @strongify(self); // __strong __typeof__(self) self = self_weak_; make.size.mas_equalTo(CGSizeMake(180, 40)); make.center.equalTo(self.view); }]; [[textField.rac_textSignal map:^id(NSString *text) { LxDBAnyVar(text); return @(text.length); }] subscribeNext:^(id x) { LxDBAnyVar(x); }]; |
map這個函式,在這裡不是地圖的意思,代表對映。map能做的事情就是把監聽的rac_textSignal
所返回的值,替換成別的就像上面程式碼中的text的長度。
filter
為了方便演示,我就不再賦值建立textField
的程式碼了,請到Demo中檢視
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[[[textField.rac_textSignal map:^id(NSString *text) { LxDBAnyVar(text); return @(text.length); }]filter:^BOOL(NSNumber *value) { return value.integerValue > 3; }] subscribeNext:^(id x) { LxDBAnyVar(x); }]; |
filter是個BOOL值,它代表的是一個條件,當這個條件發生的時候才會作出相應,比如上面程式碼中,當長度大於3的時候,才會列印x的值。
delay
1 2 3 4 5 6 7 8 9 10 11 |
//建立訊號 RACSignal * signal = [[RACSignal createSignal:^RACDisposable *(id subscriber) { [subscriber sendNext:@"rac"]; [subscriber sendCompleted]; return nil; }]delay:2]; LxPrintAnything(start); //建立訂閱者 [signal subscribeNext:^(id x) { LxDBAnyVar(x); }]; |
delay
的作用就是延遲,或者說等待,如上,等待2秒之後列印了x。
startWith
1 2 3 4 5 6 7 8 9 10 11 12 |
RACSignal * signal = [[RACSignal createSignal:^RACDisposable *(idRACSubscriber> subscriber) { // [subscriber sendNext:@"123"];//startWith:@"123"等同於這句話 也就是第一個傳送,主要是位置 [subscriber sendNext:@"rac"]; [subscriber sendCompleted]; return nil; }]startWith:@"123"]; LxPrintAnything(start); //建立訂閱者 [signal subscribeNext:^(id x) { LxDBAnyVar(x); }]; |
startWith
也就是最開始的意思,看以上程式碼 startWith:@"123"
等同於[subscriber sendNext:@"123"]
也就是第一個傳送,主要是位置.
timeOut
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
[[[RACSignal createSignal:^RACDisposable *(id subscriber) { [[RACScheduler mainThreadScheduler]afterDelay:3 schedule:^{ [subscriber sendNext:@"rac"]; [subscriber sendCompleted]; }]; return nil; }] timeout:2 onScheduler:[RACScheduler mainThreadScheduler]] subscribeNext:^(id x) { LxDBAnyVar(x); } error:^(NSError *error) { LxDBAnyVar(error); } completed:^{ LxPrintAnything(completed); }]; |
上面程式碼的意思就是,我等待3秒中傳送(afterDelay
),但是我超時了(timeout
)2秒鐘才傳送,所以這條資訊發生錯誤,會走error
的方法。 這種情況可以用在封裝http client
中,當然你可能遇到別的需求,也需要它。
take – skip
1 2 3 4 5 6 7 8 9 10 11 12 |
RACSignal * signal = [[RACSignal createSignal:^RACDisposable *(idsubscriber) { [subscriber sendNext:@"rac1"]; [subscriber sendNext:@"rac2"]; [subscriber sendNext:@"rac3"]; [subscriber sendNext:@"rac4"]; [subscriber sendCompleted]; return nil; }]take:2];//Skip [signal subscribeNext:^(id x) { LxDBAnyVar(x); }]; |
比如說我們傳送了很多次請求
take表示我們只取前兩次
skip表示跳過前兩次
takeLast表示倒數的前兩次
takeUntil這個值比較特殊,他後面的引數是個訊號,它的意思是,當takeUntil傳送這個訊號的時候,上面的傳送訊號就會停止傳送。
接下來是幾個block回撥方法
takeWhileBlock BOOL值,意思是當返回YES的時候,訂閱者才能收到訊號
skipWhileBlock BOOL值,意思是當返回YES的時候,訂閱者就會跳過訊號,NO的時候才接受
skipUntilBlock BOOL值,意思是 返回NO的時候,不會收到訊息, 直到返回YES的時候才開始收訊息。
即時搜尋優化 (throttle,distinctUntilChanged,ignore)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
UITextField * textField = [[UITextField alloc]init]; textField.backgroundColor = [UIColor cyanColor]; [self.view addSubview:textField]; @weakify(self); [textField mas_makeConstraints:^(MASConstraintMaker *make) { @strongify(self); make.size.mas_equalTo(CGSizeMake(180, 40)); make.center.equalTo(self.view); }]; //throttle 後面是個時間 表示rac_textSignal傳送訊息,0.3秒內沒有再次傳送就會相應,若是0.3內又傳送訊息了,便會在新的資訊處重新計時 //distinctUntilChanged 表示兩個訊息相同的時候,只會傳送一個請求 //ignore 表示如果訊息和ignore後面的訊息相同,則會忽略掉這條訊息,不讓其傳送 [[[[[[textField.rac_textSignal throttle:0.3] distinctUntilChanged] ignore:@""] map:^id(id value) { return [RACSignal createSignal:^RACDisposable *(id subscriber) { // network request [subscriber sendNext:value]; [subscriber sendCompleted]; return [RACDisposable disposableWithBlock:^{ // cancel request }]; }]; }]switchToLatest] subscribeNext:^(id x) { LxDBAnyVar(x); }]; |
以上程式碼,是用textField模擬一個即時搜尋優化的功能,其中引數如下:
throttle 後面是個時間 表示rac_textSignal傳送訊息,0.3秒內沒有再次傳送就會相應,若是0.3內又傳送訊息了,便會在新的資訊處重新計時
distinctUntilChanged 表示兩個訊息相同的時候,只會傳送一個請求
ignore 表示如果訊息和ignore後面的訊息相同,則會忽略掉這條訊息,不讓其傳送
這樣做,是不是給伺服器減小了很多的壓力,更是節省了我們大量的程式碼。 其中我們用map建立了一個新的訊號,我們知道textField的改變是一個訊號, map就是在這個訊號上,又加了一個訊號,即signal of signals
。
訂閱者所列印的訊息x則是,map發出的訊號。我們可以再map中傳送新的訊號,以及取消訊號disposable
.
當我們用map傳送訊號的時候,我們則需要使用 switchToLatest
這個引數來獲取最後一個訊號,也就是我們最後所列印的x,就是map最後發錯的這個訊號。
repeat
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[[[[[RACSignal createSignal:^RACDisposable *(id subscriber) { [subscriber sendNext:@"rac"]; [subscriber sendCompleted]; return nil; }]delay:1]repeat]take:3] subscribeNext:^(id x) { LxDBAnyVar(x); } completed:^{ LxPrintAnything(completed); }]; |
repeat,顧名思義,就是重複傳送這條訊息,當我們在後面新增了delay和take的時候,意思就是每隔1秒傳送一次這條訊息,傳送3次後停止。
merge – concat – zipWith
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
RACSignal * signalA = [RACSignal createSignal:^RACDisposable *(idsubscriber) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ LxPrintAnything(a); [subscriber sendNext:@"a"]; [subscriber sendCompleted]; }); return nil; }]; RACSignal * signalB = [RACSignal createSignal:^RACDisposable *(idsubscriber) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ LxPrintAnything(b); [subscriber sendNext:@"b"]; [subscriber sendCompleted]; }); return nil; }]; [[RACSignal merge:@[signalA, signalB]]subscribeNext:^(id x) { LxDBAnyVar(x); }]; |
我們建立了兩個請求,A和B,用GCD的方法A延遲兩秒鐘,B延遲了3秒鐘,我們用merge
方法合併了A和B,列印結果為
1 2 3 4 |
?__23-[ViewController merge]_block_invoke_2 + 66? a ?__23-[ViewController merge]_block_invoke29 + 87? x = a ?__23-[ViewController merge]_block_invoke_215 + 77? b ?__23-[ViewController merge]_block_invoke29 + 87? x = b |
也就是A和B不管誰傳送都會列印x,簡單的說就是A和B的列印方法用的是同一個。他們之間關係是獨立的,如果A傳送失敗,B依然會執行。
當我們用concat
方法連結A和B之後,意思就是當A執行完了之後才會執行B,他們之間是依賴的關係,如果A傳送失敗,B也不會執行。
請注意合併(merge)和連結(concat)的區別。
zipWith
,當用zipWith
連結A和B的時候,只有在A.B每隔都至少傳送過一次訊息的時候才會執行zipWith的方法,它的返回值是一個集合,也就是陣列,同時包含了A和B的列印結果。
zipWith
的寫法等同於 :
1 2 3 4 |
[[RACSignal combineLatestWith:@[signalA, signalB]subscribeNext:^(id x) { LxDBAnyVar(x); }]; |
亦或者
1 2 3 4 |
[[RACSignal combineLatest:@[signalA, signalB]]subscribeNext:^(id x) { LxDBAnyVar(x); }]; |
但是使用combineLatest
,可以再後面新增更多的訊號.
RAC() 巨集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//button setBackgroundColor:forState: UIButton * button = [UIButton buttonWithType:UIButtonTypeCustom]; [self.view addSubview:button]; @weakify(self); [button mas_makeConstraints:^(MASConstraintMaker *make) { @strongify(self); make.size.mas_equalTo(CGSizeMake(180, 40)); make.center.equalTo(self.view); }]; RAC(button, backgroundColor) = [RACObserve(button, selected) map:^UIColor *(NSNumber * selected) { return [selected boolValue] ? [UIColor redColor] : [UIColor greenColor]; }]; [[button rac_signalForControlEvents:UIControlEventTouchUpInside]subscribeNext:^(UIButton * btn) { btn.selected = !btn.selected; }]; |
比如Btn的設定背景顏色的屬性,OC中並沒有button setBackgroundColor:forState:
這種方法,我們不能直接設定其選中後的顏色。在RAC中,則可以很簡單的改變BTN的背景顏色。不得不說RAC的簡單和強大。
做一個秒錶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
UILabel * label = ({ UILabel * label = [[UILabel alloc]init]; label.backgroundColor = [UIColor cyanColor]; label; }); [self.view addSubview:label]; @weakify(self); [label mas_makeConstraints:^(MASConstraintMaker *make) { @strongify(self); make.size.mas_equalTo(CGSizeMake(240, 40)); make.center.equalTo(self.view); }]; RAC(label, text) = [[RACSignal interval:1 onScheduler:[RACScheduler mainThreadScheduler]] map:^NSString *(NSDate * date) { return date.description; }]; |
只有這麼多程式碼,我們便可以完美的做一個秒錶,是否很cool?
結束
當我們大量使用RAC寫程式碼的時候,會把一個個事件封裝成一個個訊號,通過觸發訊號,訂閱這個訊號來返回各種資訊。RAC使我們的程式碼耦合性根底,聚合性更高。
若有不懂得地方可以留言,若有寫錯的地方,請及時與我聯絡,可以留言或者Email等。
文字所用的Demo,下載地址 戳這裡.