使用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/


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章