ReactiveCocoa代碼實踐之-更多思考,eactivecocoa
三.ReactiveCocoa代碼實踐之-更多思考
1. RACObserve()宏形參寫法的區別
之前寫代碼考慮過 RACObserve(self.timeLabel , text) 和 RACObserve(self , timeLabel.text) 的區別。 因為這兩種方法都是觀察self.timeLabel.text的屬性,並且都能實現功能。估計是作者原本用的其中一種後來對另一種也提供了支援,究竟有什麼區別哪一種寫法更好?
點進去看RACObserve的源碼 大多都是方法調用,一層一層點進去最後來到這個方法。
- (RACDisposable *)rac_observeKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options observer:(__weak NSObject *)weakObserver block:(void (^)(id, NSDictionary *, BOOL, BOOL))block
這個方法裡面把逗號後面的keypath通過“.” 進行分割成了一個數組。 並且得到三個屬性
BOOL keyPathHasOneComponent = (keyPathComponents.count == 1);NSString *keyPathHead = keyPathComponents[0];NSString *keyPathTail = keyPath.rac_keyPathByDeletingFirstKeyPathComponent;
這裡會取到keypatch的頭和去迴轉的部分,並且會在下面方法內部自己調用自己
// Adds the callback block to the remaining path components on the value. Also// adds the logic to clean up the callbacks to the firstComponentDisposable.void (^addObserverToValue)(NSObject *) = ^(NSObject *value) { RACDisposable *observerDisposable = [value rac_observeKeyPath:keyPathTail options:(options & ~NSKeyValueObservingOptionInitial) observer:weakObserver block:block]; [firstComponentDisposable() addDisposable:observerDisposable];};
並且把keyPathTail 作為keypatch傳進去了,就是遞迴調用,每一次進來都會切掉第一個元素,直到BOOL keyPathHasOneComponent 這個值等於yes。從這個角度看用RACObserve(self , timeLabel.text) 這種寫法會引發遞迴調用,效能不如RACObserve(self.timeLabel.text)。
更多RAC宏相關知識可見這篇 :http://blog.sunnyxx.com/2014/03/06/rac_1_macros/
2.集合操作
假設現在有一個需求,有一串密碼的數組,我們判斷密碼長度小於6位就是太短,就會系統內部拋出一個訊息:XXX密碼太短不合格。採用RAC的寫法會比常規寫法方便,一個過濾一個自訂然後直接返回。
NSArray *pwds = @[@"567887",@"89877",@"789",@"7899000"];RACSequence *results = [[pwds.rac_sequence filter:^ BOOL (NSString *pwd) { return pwd.length < 6; }]map:^id(NSString *pwd) { return [[pwd mutableCopy]stringByAppendingString:@"密碼太短不合格"]; }];NSLog(@"%@",results.array);
中間filter方法的block內代碼會在下面results.array代碼執行時才會執行, 相當於是有了個訂閱者才會執行。這一點和RACSignal很像,因為signal 和 sequence 都是streams,他們共用很多相同的方法signal是push驅動的stream,sequence是pull驅動的stream。
如果相從RACSequence對象中取出其他屬性時進行操作也可以用如下方法
RACSequence *s = [RACSequence sequenceWithHeadBlock:^id{ return @"自訂動作";} tailBlock:^RACSequence *{ return [RACSequence new];}];NSLog(@"%@",s.head);NSLog(@"%@",s.tail);
兩個block分別會在指定屬性被調用時才會執行,注意head就是sequence的第一個元素,而tail是除去第一個元素的剩餘所有,所以還是一個sequence。(董鉑然部落格園)
3.訊號實現遊戲技能釋放
假設現在需要用RAC類比一個街機裡放爆氣技能的方法。 按下了指定的按鈕順序下前下前拳就會釋放絕招。
首先需要將各個按鈕連線,並設定一個訊號來監聽所有按鍵單獨訊號的並集,捕捉到每個按鈕的title。
// 把六個按鍵的訊號合并RACSignal *comboSignal = [[RACSignal merge:@[ [self.topBtn rac_signalForControlEvents:UIControlEventTouchUpInside], [self.bottomBtn rac_signalForControlEvents:UIControlEventTouchUpInside], [self.leftBtn rac_signalForControlEvents:UIControlEventTouchUpInside], [self.rightBtn rac_signalForControlEvents:UIControlEventTouchUpInside], [self.BBtn rac_signalForControlEvents:UIControlEventTouchUpInside], [self.ABtn rac_signalForControlEvents:UIControlEventTouchUpInside]]]map:^id(UIButton *btn) { return btn.currentTitle;}];
然後對這個訊號源進行buffer操作,把每三秒收到的所有按鍵資訊都捕獲到,並進行判斷和後繼操作
// 設定觸發爆氣條件NSString *comboCode = @"下前下前拳";// 實際操作RACSignal *canAction = [[[comboSignal bufferWithTime:3 onScheduler:[RACScheduler mainThreadScheduler]] map:^id(RACTuple *value) { return [[value allObjects] componentsJoinedByString:@""];}] map:^id(NSString *value) { return @([value containsString:comboCode]);}];// 調用combo:方法就是技能釋放[self rac_liftSelector:@selector(combo:) withSignalsFromArray:@[canAction]];
上面的代碼可以實現預計的功能,只要你能在三秒的buffer內按出指定的按鍵就能釋放。但是用這個方法中間也有一個問題:設定了buffer3秒後這個block裡面每隔三秒才會來到一次,也就是說如果你在0.5秒內就按出了技能,那也需要再等2.5秒才能放出技能,顯然這個在實戰中是不能接受的。
於是嘗試了其他的實現思路,嘗試了takeLast:及takeUntilBlock:及scanWithStart: 等方法都不是很合適,最後使用了aggregateWithStart: 達到了需求的目的。
// 設定觸發爆氣條件NSString *comboCode = @"下前下前拳";// 實際操作_time = [[[NSDate alloc] init] timeIntervalSince1970];[[comboSignal aggregateWithStart:@"" reduce:^id(NSString* running, NSString* next) { if (([[[NSDate alloc] init] timeIntervalSince1970] - _time) < 3){ NSString *str = [NSString stringWithFormat:@"%@%@",running,next]; return [str containsString:comboCode]?[self combo]:str; } _time = [[[NSDate alloc] init] timeIntervalSince1970]; return str.length < combo.length ? str : [str subStringFromIndex:str.length - comboCode.length];}]subscribeNext:^(id x){}];
使用這段代碼可以在滿足之前條件的前提下,並且按鈕一按完馬上觸發技能。
aggregateWithStart:reduce:的第一個參數是初始值,第二個參數是一個block,這個block的傳回值就是下一次來到這個block的 running參數。我在這個block的迴圈中做的操作有:
1.對時間進行delta計算,如果距離上一次時間節點大於3秒,重新整理時間節點重新計時。 str小於5則返回,大於5則截取後五位返回。
2.如果小於3秒則把每次按鍵資訊彙總成一個字串並判斷是否包含技能觸發代碼。
3.滿足的話觸發技能,技能方法的內部也重新整理了時間節點,並截取running(保留最後4位,防止上一個迴圈結束和下一個迴圈開始所滿足的條件)。不滿足則將這個字串繼續返回。
雖然代碼寫的不是很好看,但是功能是實現了,感覺有點彆扭,因為函數式編程倡導的是引用透明無副作用,所以上面需要記錄值和成員變數的做法很明顯就不適合用RAC了,應該還會有更好的方法實現。
4.其他RAC操作
1)映射:flattenMap,Map用於把源訊號內容映射成新的內容
2)組合:concat:按一定順序拼接訊號,當多個訊號發出的時候,有順序的接收訊號
3)`then`:用於串連兩個訊號,當第一個訊號完成,才會串連then返回的訊號
4)`merge`:把多個訊號合并為一個訊號,任何一個訊號有新值的時候就會調用
5)`combineLatest`:將多個訊號合并起來,並且拿到各個訊號的最新的值,必須每個合并的signal至少都有過一次sendNext,才會觸發合并的訊號。
6)`reduce`彙總:用於訊號發出的內容是元組,把訊號發出元組的值彙總成一個值
7)filter:過濾訊號,使用它可以擷取滿足條件的訊號.
8) ignore:忽略完某些值的訊號.
9) distinctUntilChanged:當上一次的值和當前的值有明顯的變化就會發出訊號,否則會被忽略掉
10) take:從開始一共取N次的訊號
11)takeLast:取最後N次的訊號,前提條件,訂閱者必須調用完成,因為只有完成,就知道總共有多少訊號
12)takeUntil:(RACSignal *):擷取訊號直到某個訊號執行完成
13)skip:(NSUInteger):跳過幾個訊號,不接受
14)switchToLatest:用於signalOfSignals(訊號的訊號),有時候訊號也會發出訊號,會在signalOfSignals中,擷取signalOfSignals發送的最新訊號
15)doNext: 執行Next之前,會先執行這個Block
16)doCompleted: 執行sendCompleted之前,會先執行這個Block
17)deliverOn: 內容傳遞切換到制定線程中,副作用在原來線程中,把在建立訊號時block中的代碼稱之為副作用
18)subscribeOn: 內容傳遞和副作用都會切換到制定線程中
19)interval 定時:每隔一段時間發出訊號
20)delay 延遲發送next。
21) 代替代理:
- rac_signalForSelector:用於替代代理。
22) 代替KVO :
- rac_valuesAndChangesForKeyPath:用於監聽某個對象的屬性改變。
23) 監聽事件:
- rac_signalForControlEvents:用於監聽某個事件。
24) 代替通知:
- rac_addObserverForName:用於監聽某個通知。
25) 監聽文字框文字改變:
- rac_textSignal:只要文字框發出改變就會發出這個訊號。
26) 處理當介面有多次請求時,需要都擷取到資料時,才能展示介面
- rac_liftSelector:withSignalsFromArray:Signals:當傳入的Signals(訊號數組),每一個signal都至少sendNext過一次,就會去觸發第一個selector參數的方法。
- 使用注意:幾個訊號,參數一的方法就幾個參數,每個參數對應訊號發出的資料
RAC曾經被冠以 學習成本搞,可讀性差,debug的噩夢等不良評價,但隨著近幾年的演變已逐漸被企業階層專案所接受,並且成為函數響應式編程主流架構。RAC用人越來越多,隨筆和部落格也越來越多,學習的門檻已經大大降低。 並且我覺得初學者沒有必要一開始就把所有操作和概念都弄懂,可以從簡單的用法開始一步步的接觸高階文法,這樣會更容易接受。