天天看點

使用ReactiveCocoa實作iOS平台響應式程式設計使用ReactiveCocoa實作iOS平台響應式程式設計

使用ReactiveCocoa實作iOS平台響應式程式設計

ReactiveCocoa和響應式程式設計

在說ReactiveCocoa之前,先要介紹一下FRP(Functional Reactive Programming,響應式程式設計),在維基百科中有這樣一個例子介紹:

在指令式程式設計環境中,a = b + c 表示将表達式的結果賦給a,而之後改變b或c的值不會影響a。但在響應式程式設計中,a的值會随着b或c的更新而更新。

Excel就是響應式程式設計的一個例子。單元格可以包含字面值或類似”=B1+C1″的公式,而包含公式的單元格的值會依據其他單元格的值的變化而變化 。

而ReactiveCocoa簡稱RAC,就是基于響應式程式設計思想的Objective-C實踐,它是Github的一個開源項目,你可以在這裡找到它。

關于FRP和ReactiveCocoa可以去看leezhong的這篇blog,圖文并茂,講的很好。

ReactiveCocoa架構概覽

先來看一下leezhong再博文中提到的比喻,讓你對有個ReactiveCocoa很好的了解:

可以把信号想象成水龍頭,隻不過裡面不是水,而是玻璃球(value),直徑跟水管的内徑一樣,這樣就能保證玻璃球是依次排列,不會出現并排的情況(資料都是線性處理的,不會出現并發情況)。水龍頭的開關預設是關的,除非有了接收方(subscriber),才會打開。這樣隻要有新的玻璃球進來,就會自動傳送給接收方。可以在水龍頭上加一個過濾嘴(filter),不符合的不讓通過,也可以加一個改動裝置,把球改變成符合自己的需求(map)。也可以把多個水龍頭合并成一個新的水龍頭(combineLatest:reduce:),這樣隻要其中的一個水龍頭有玻璃球出來,這個新合并的水龍頭就會得到這個球。

下面我來逐一介紹ReactiveCocoa架構的每個元件

Streams

Streams 表現為RACStream類,可以看做是水管裡面流動的一系列玻璃球,它們有順序的依次通過,在第一個玻璃球沒有到達之前,你沒法獲得第二個玻璃球。

RACStream描述的就是這種線性流動玻璃球的形态,比較抽象,它本身的使用意義并不很大,一般會以signals或者sequences等這些更高層次的表現形态代替。

Signals

Signals 表現為RACSignal類,就是前面提到水龍頭,ReactiveCocoa的核心概念就是Signal,它一般表示未來要到達的值,想象玻璃球一個個從水龍頭裡出來,隻有了接收方(subscriber)才能擷取到這些玻璃球(value)。

Signal會發送下面三種事件給它的接受方(subscriber),想象成水龍頭有個訓示燈來彙報它的工作狀态,接受方通過

-subscribeNext:error:completed:

對不同僚件作出相應反應

  • next 從水龍頭裡流出的新玻璃球(value)
  • error 擷取新的玻璃球發生了錯誤,一般要發送一個NSError對象,表明哪裡錯了
  • completed 全部玻璃球已經順利抵達,沒有更多的玻璃球加入了

一個生命周期的Signal可以發送任意多個“next”事件,和一個“error”或者“completed”事件(當然“error”和“completed”隻可能出現一種)

Subjects

subjects 表現為RACSubject類,可以認為是“可變的(mutable)”信号/自定義信号,它是嫁接非RAC代碼到Signals世界的橋梁,很有用。嗯。。。 這樣講還是很抽象,舉個例子吧:

1 2 3 RACSubject *letters = [RACSubject subject]; RACSignal *signal = [letters sendNext:@"a"];  

可以看到

@"a"

隻是一個NSString對象,要想在水管裡順利流動,就要借RACSubject的力。

Commands

command 表現為RACCommand類,偷個懶直接舉個例子吧,比如一個簡單的注冊界面:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21     RACSignal *formValid=[RACSignal         combineLatest:@[             self.userNameField.rac_textSignal,             self.emailField.rac_textSignal,         ]         reduce:^(NSString *userName,NSString *email){             return@(userName.length>0                     &&email.length>0);         }];     RACCommand *createAccountCommand=[RACCommandcommandWithCanExecuteSignal:formValid];   RACSignal *networkResults=[[[createAccountCommand       addSignalBlock:^RACSignal *(idvalue){           //... 網絡互動代碼       }]       switchToLatest]       deliverOn:[RACSchedulermainThreadScheduler]];     // 綁定建立按鈕的 UI state 和點選事件     [[self.createButtonrac_signalForControlEvents:UIControlEventTouchUpInside]executeCommand:createAccountCommand];  

Sequences

sequence 表現為RACSequence類,可以簡單看做是RAC世界的NSArray,RAC增加了

-rac_sequence

方法,可以使諸如NSArray這些集合類(collection classes)直接轉換為RACSequence來使用。

Schedulers

scheduler 表現為RACScheduler類,類似于GCD,but schedulers support cancellationbut schedulers support cancellation, and always execute serially.

ReactiveCocoa的簡單使用

實踐出真知,下面就舉一些簡單的例子,一起看看RAC的使用

Subscription

接收 

-subscribeNext:

-subscribeError:

-subscribeCompleted:

1 2 3 4 5 6 7 RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;   // 依次輸出 A B C D… [letters subscribeNext:^(NSString *x) {     NSLog(@"%@", x); }];  

Injecting effects

注入效果 

-doNext:

-doError:

-doCompleted:

,看下面注釋應該就明白了:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 __blockunsignedsubscriptions=0;   RACSignal *loggingSignal=[RACSignalcreateSignal:^RACDisposable *(id<RACSubscriber>subscriber){     subscriptions++;     [subscribersendCompleted];     returnnil; }];   // 不會輸出任何東西 loggingSignal=[loggingSignaldoCompleted:^{     NSLog(@"about to complete subscription %u",subscriptions); }];   // 輸出: // about to complete subscription 1 // subscription 1 [loggingSignalsubscribeCompleted:^{     NSLog(@"subscription %u",subscriptions); }];  

Mapping

-map:

 映射,可以看做對玻璃球的變換、重新組裝

1 2 3 4 5 6 7 RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;   // Contains: AA BB CC DD EE FF GG HH II RACSequence *mapped = [letters map:^(NSString *value) {     return [value stringByAppendingString:value]; }];  

Filtering

-filter:

 過濾,不符合要求的玻璃球不允許通過

1 2 3 4 5 6 7 RACSequence *numbers=[@"1 2 3 4 5 6 7 8 9"componentsSeparatedByString:@" "].rac_sequence;   // Contains: 2 4 6 8 RACSequence *filtered=[numbersfilter:^BOOL(NSString *value){     return(value.intValue%2)==0; }];  

Concatenating

-concat:

 把一個水管拼接到另一個水管之後

1 2 3 4 5 6 RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence; RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;   // Contains: A B C D E F G H I 1 2 3 4 5 6 7 8 9 RACSequence *concatenated = [letters concat:numbers];  

Flattening

-flatten:

Sequences are concatenated

1 2 3 4 5 6 7 RACSequence *letters=[@"A B C D E F G H I"componentsSeparatedByString:@" "].rac_sequence; RACSequence *numbers=[@"1 2 3 4 5 6 7 8 9"componentsSeparatedByString:@" "].rac_sequence; RACSequence *sequenceOfSequences=@[letters,numbers].rac_sequence;   // Contains: A B C D E F G H I 1 2 3 4 5 6 7 8 9 RACSequence *flattened=[sequenceOfSequencesflatten];  

Signals are merged (merge可以了解成把幾個水管的龍頭合并成一個,哪個水管中的玻璃球哪個先到先吐哪個玻璃球)

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 RACSubject *letters = [RACSubject subject]; RACSubject *numbers = [RACSubject subject]; RACSignal *signalOfSignals = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {     [subscriber sendNext:letters];     [subscriber sendNext:numbers];     [subscriber sendCompleted];     return nil; }];   RACSignal *flattened = [signalOfSignals flatten];   // Outputs: A 1 B C 2 [flattened subscribeNext:^(NSString *x) {     NSLog(@"%@", x); }];   [letters sendNext:@"A"]; [numbers sendNext:@"1"]; [letters sendNext:@"B"]; [letters sendNext:@"C"]; [numbers sendNext:@"2"];  

Mapping and flattening

-flattenMap:

 先 map 再 flatten

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 RACSequence *numbers=[@"1 2 3 4 5 6 7 8 9"componentsSeparatedByString:@" "].rac_sequence;   // Contains: 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 RACSequence *extended=[numbersflattenMap:^(NSString *num){     return@[num,num].rac_sequence; }];   // Contains: 1_ 3_ 5_ 7_ 9_ RACSequence *edited=[numbersflattenMap:^(NSString *num){     if(num.intValue%2==0){         return[RACSequenceempty];     }else{         NSString *newNum=[numstringByAppendingString:@"_"];         return[RACSequencereturn:newNum];     } }];         RACSignal *letters=[@"A B C D E F G H I"componentsSeparatedByString:@" "].rac_sequence.signal;   [[letters     flattenMap:^(NSString *letter){         return[databasesaveEntriesForLetter:letter];     }]     subscribeCompleted:^{         NSLog(@"All database entries saved successfully.");     }];  

Sequencing

-then:

1 2 3 4 5 6 7 8 9 10 11 12 13 RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;   // 新水龍頭隻包含: 1 2 3 4 5 6 7 8 9 // // 但當有接收時,仍會執行舊水龍頭doNext的内容,是以也會輸出 A B C D E F G H I RACSignal *sequenced = [[letters     doNext:^(NSString *letter) {         NSLog(@"%@", letter);     }]     then:^{         return [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence.signal;     }];  

Merging

+merge:

 前面在flatten中提到的水龍頭的合并

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 RACSubject *letters=[RACSubjectsubject]; RACSubject *numbers=[RACSubjectsubject]; RACSignal *merged=[RACSignalmerge:@[letters,numbers]];   // Outputs: A 1 B C 2 [mergedsubscribeNext:^(NSString *x){     NSLog(@"%@",x); }];   [letterssendNext:@"A"]; [numberssendNext:@"1"]; [letterssendNext:@"B"]; [letterssendNext:@"C"]; [numberssendNext:@"2"];  

Combining latest values

+combineLatest:

 任何時刻取每個水龍頭吐出的最新的那個玻璃球

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 RACSubject *letters = [RACSubject subject]; RACSubject *numbers = [RACSubject subject]; RACSignal *combined = [RACSignal     combineLatest:@[ letters, numbers ]     reduce:^(NSString *letter, NSString *number) {         return [letter stringByAppendingString:number];     }];   // Outputs: B1 B2 C2 C3 [combined subscribeNext:^(id x) {     NSLog(@"%@", x); }];   [letters sendNext:@"A"]; [letters sendNext:@"B"]; [numbers sendNext:@"1"]; [numbers sendNext:@"2"]; [letters sendNext:@"C"]; [numbers sendNext:@"3"];  

Switching

-switchToLatest:

 取指定的那個水龍頭的吐出的最新玻璃球

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 RACSubject *letters=[RACSubjectsubject]; RACSubject *numbers=[RACSubjectsubject]; RACSubject *signalOfSignals=[RACSubjectsubject];   RACSignal *switched=[signalOfSignalsswitchToLatest];   // Outputs: A B 1 D [switchedsubscribeNext:^(NSString *x){     NSLog(@"%@",x); }];   [signalOfSignalssendNext:letters]; [letterssendNext:@"A"]; [letterssendNext:@"B"];   [signalOfSignalssendNext:numbers]; [letterssendNext:@"C"]; [numberssendNext:@"1"];   [signalOfSignalssendNext:letters]; [numberssendNext:@"2"]; [letterssendNext:@"D"];  

常用宏

RAC 可以看作某個屬性的值與一些信号的關聯
1 2 3 4 RAC(self.submitButton.enabled) = [RACSignal combineLatest:@[self.usernameField.rac_textSignal, self.passwordField.rac_textSignal]reduce:^id(NSString *userName, NSString *password) {     return @(userName.length >= 6 && password.length >= 6); }];  
RACObserve 監聽屬性的改變,使用block的KVO
1 2 3 4 [RACObserve(self.textField,text)subscribeNext:^(NSString *newName){     NSLog(@"%@",newName); }];  

UI Event

RAC為系統UI提供了很多category,非常棒,比如UITextView、UITextField文本框的改動

rac_textSignal

,UIButton的的按下

rac_command

等等。

最後

有了RAC,可以不用去操心值什麼時候到達什麼時候改變,隻需要簡單的進行資料來了之後的步驟就可以了。

說了這麼多,在回過頭去看leezhong的比喻和該文最後總結的關系圖,再好好梳理一下吧。我也是初學者,誠惶誠恐的呈上這篇博文,歡迎讨論,如有不正之處歡迎批評指正。

參考

https://github.com/ReactiveCocoa/ReactiveCocoa

https://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/Documentation/FrameworkOverview.md

https://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/Documentation/BasicOperators.md

http://vimeo.com/65637501

http://iiiyu.com/2013/09/11/learning-ios-notes-twenty-eight/

http://blog.leezhong.com/ios/2013/06/19/frp-reactivecocoa.htmlhttp://nshipster.com/reactivecocoa/