使用ReactiveCocoa實現iOS平臺響應式編程 – iPhone手機開發技術文章 iPhone軟體開發教學課程

使用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世界的橋梁,很有用。嗯。。。 這樣講還是很抽象,舉個例子吧:

    123 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
    =
    [RACCommand
    commandWithCanExecuteSignal:formValid];

    RACSignal *networkResults
    =
    [[[createAccountCommand

    addSignalBlock:^RACSignal
    *(id
    value)
    {

    //… 網絡交互代碼

    }]

    switchToLatest]

    deliverOn:[RACScheduler
    mainThreadScheduler]];

    // 綁定創建按鈕的 UI state 和點擊事件

    [[self.createButton
    rac_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:

    1234567 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

    __block
    unsigned
    subscriptions
    =
    0;

    RACSignal *loggingSignal
    =
    [RACSignal
    createSignal:^
    RACDisposable *
    (id<RACSubscriber>
    subscriber)
    {

    subscriptions++;

    [subscriber
    sendCompleted];

    return
    nil;

    }];

    // 不會輸出任何東西

    loggingSignal
    =
    [loggingSignal
    doCompleted:^{

    NSLog(@”about
    to complete subscription %u”,
    subscriptions);

    }];

    // 輸出:

    // about to complete subscription 1

    // subscription 1

    [loggingSignal
    subscribeCompleted:^{

    NSLog(@”subscription
    %u”,
    subscriptions);

    }];

    Mapping

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

    1234567 RACSequence *letters = [@”A B C D E F G H I” componentsSeparatedByString:@” “].rac_sequence; // Contains: AA BB CC DD EE FF GG HH IIRACSequence *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
    =
    [numbers
    filter:^
    BOOL
    (NSString
    *value)
    {

    return
    (value.intValue
    %
    2)
    ==
    0;

    }];

    Concatenating

    -concat: 把一個水管拼接到另一個水管之後

    123456 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 9RACSequence *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
    =
    [sequenceOfSequences
    flatten];

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

    12345678910111213141516171819202122 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
    =
    [numbers
    flattenMap:^(NSString
    *num)
    {

    return
    @[
    num,
    num
    ].rac_sequence;

    }];

    // Contains: 1_ 3_ 5_ 7_ 9_

    RACSequence *edited
    =
    [numbers
    flattenMap:^(NSString
    *num)
    {

    if
    (num.intValue
    %
    2
    ==
    0)
    {

    return
    [RACSequence
    empty];

    }
    else
    {

    NSString
    *newNum
    =
    [num
    stringByAppendingString:@”_”];

    return
    [RACSequence
    return:newNum];

    }

    }];

    RACSignal *letters
    =
    [@”A
    B C D E F G H I”
    componentsSeparatedByString:@”
    “].rac_sequence.signal;

    [[letters

    flattenMap:^(NSString
    *letter)
    {

    return
    [database
    saveEntriesForLetter:letter];

    }]

    subscribeCompleted:^{

    NSLog(@”All
    database entries saved successfully.”);

    }];

    Sequencing

    -then:

    12345678910111213 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 IRACSignal *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
    =
    [RACSubject
    subject];

    RACSubject *numbers
    =
    [RACSubject
    subject];

    RACSignal *merged
    =
    [RACSignal
    merge:@[
    letters,
    numbers
    ]];

    // Outputs: A 1 B C 2

    [merged
    subscribeNext:^(NSString
    *x)
    {

    NSLog(@”%@”,
    x);

    }];

    [letters
    sendNext:@”A”];

    [numbers
    sendNext:@”1″];

    [letters
    sendNext:@”B”];

    [letters
    sendNext:@”C”];

    [numbers
    sendNext:@”2″];

    Combining latest values

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

    1234567891011121314151617181920 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
    =
    [RACSubject
    subject];

    RACSubject *numbers
    =
    [RACSubject
    subject];

    RACSubject *signalOfSignals
    =
    [RACSubject
    subject];

    RACSignal *switched
    =
    [signalOfSignals
    switchToLatest];

    // Outputs: A B 1 D

    [switched
    subscribeNext:^(NSString
    *x)
    {

    NSLog(@”%@”,
    x);

    }];

    [signalOfSignals
    sendNext:letters];

    [letters
    sendNext:@”A”];

    [letters
    sendNext:@”B”];

    [signalOfSignals
    sendNext:numbers];

    [letters
    sendNext:@”C”];

    [numbers
    sendNext:@”1″];

    [signalOfSignals
    sendNext:letters];

    [numbers
    sendNext:@”2″];

    [letters
    sendNext:@”D”];

    常用宏

    RAC 可以看作某個屬性的值與一些信號的聯動

    1234 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

    https://vimeo.com/65637501
    https://iiiyu.com/2013/09/11/learning-ios-notes-twenty-eight/
    https://blog.leezhong.com/ios/2013/06/19/frp-reactivecocoa.htmlhttps://nshipster.com/reactivecocoa/

You May Also Like