RAC 中的雙向數據綁定 RACChannel

KelChewning 7年前發布 | 16K 次閱讀 UIKit iOS開發 移動開發 Oracle RAC

之前講過了 ReactiveCocoa 中的一對一的單向數據流 RACSignal 和一對多的單向數據流 RACMulticastConnection ,這一篇文章分析的是一對一的雙向數據流 RACChannel 。

RACChannel 其實是一個相對比較復雜的類,但是,對其有一定了解之后合理運用的話,會在合適的業務中提供非常強大的支持能夠極大的簡化業務代碼。

RACChannel 簡介

RACChannel 可以被理解為一個雙向的連接,這個連接的兩端都是 RACSignal 實例,它們可以向彼此發送消息,如果我們在視圖和模型之間通過 RACChannel 建立這樣的連接:

那么從模型發出的消息,最后會發送到視圖上;反之,用戶對視圖進行的操作最后也會體現在模型上。這種通信方式的實現是基于信號的, RACChannel 內部封裝了兩個 RACChannelTerminal 對象,它們都是 RACSignal 的子類:

對模型進行的操作最后都會發送給 leadingTerminal 再通過內部的實現發送給 followingTerminal ,由于視圖是 followingTerminal 的訂閱者,所以消息最終會發送到視圖上。

在上述情況下, leadingTerminal 的訂閱者(模型)并不會收到消息,它的訂閱者(視圖)只會在 followingTerminal 收到消息時才會接受到新的值。

同時, RACChannel 的綁定都是雙向的,視圖收到用戶的動作,例如點擊等事件時,會將消息發送給 followingTerminal ,而 followingTerminal 并 不會 將消息發送給自己的訂閱者(視圖),而是會發送給 leadingTerminal ,并通過 leadingTerminal 發送給其訂閱者,即模型。

上圖描述了信息在 RACChannel 之間的傳遞過程,無論是模型屬性的改變還是用戶對視圖進行的操作都會通過這兩個 RACChannelTerminal 傳遞到另一端;同時,由于消息不會發送給自己的訂閱者,所以不會造成信息的循環發送。

RACChannel 和 RACChannelTerminal

RACChannel 和 RACChannelTerminal 的關系非常密切,前者可以理解為一個網絡連接,后者可以理解為 socket ,表示網絡連接的一端,下圖描述了 RACChannel 與網絡連接中概念的一一對應關系。

  • 在客戶端使用 write 向 socket 中發送消息時, socket 的持有者客戶端不會收到消息,只有在 socket 上調用 read 的服務端才會收到消息;反之亦然。
  • 在模型使用 sendNext 向 leadingTerminal 中發送消息時, leadingTerminal 的訂閱者模型不會收到消息,只有在 followingTerminal 上調用 subscribe 的視圖才會收到消息;反之亦然。

RACChannelTerminal 的實現

為什么向 RACChannelTerminal 發送消息,它的訂閱者獲取不到?先來看一下它在頭文件中的定義:

@interface RACChannelTerminal : RACSignal <RACSubscriber>
@end

RACChannelTerminal 是一個信號的子類,同時它還遵循了 RACSubscriber 協議,也就是可以向它調用 -sendNext: 等方法; RAChannelTerminal 中持有了兩個對象:

在初始化時,需要傳入 values 和 otherTerminal 這兩個屬性,其中 values 表示當前斷點, otherTerminal 表示遠程端點:

- (instancetype)initWithValues:(RACSignal *)values otherTerminal:(id<RACSubscriber>)otherTerminal {
    self = [super init];
    _values = values;
    _otherTerminal = otherTerminal;
    return self;
}

當然,作為 RACSignal 的子類, RACChannelTerminal 必須覆寫 -subscribe: 方法:

- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {
    return [self.values subscribe:subscriber];
}

在訂閱者調用 -subscribeNext: 等方法發起訂閱時,實際上訂閱的是當前端點;如果向當前端點發送消息,會被轉發到遠程端點上,而這也就是當前端點的訂閱者不會接收到向當前端點發送消息的原因:

- (void)sendNext:(id)value {
    [self.otherTerminal sendNext:value];
}
- (void)sendError:(NSError *)error {
    [self.otherTerminal sendError:error];
}
- (void)sendCompleted {
    [self.otherTerminal sendCompleted];
}

RACChannel 的初始化

我們在任何情況下都不應該直接使用 -init 方法初始化 RACChannelTerminal 的實例,而是應該以創建 RACChannel 的方式生成它:

- (instancetype)init {
    self = [super init];

    RACReplaySubject *leadingSubject = [RACReplaySubject replaySubjectWithCapacity:0];
    RACReplaySubject *followingSubject = [RACReplaySubject replaySubjectWithCapacity:1];

    [[leadingSubject ignoreValues] subscribe:followingSubject];
    [[followingSubject ignoreValues] subscribe:leadingSubject];

    _leadingTerminal = [[RACChannelTerminal alloc] initWithValues:leadingSubject otherTerminal:followingSubject];
    _followingTerminal = [[RACChannelTerminal alloc] initWithValues:followingSubject otherTerminal:leadingSubject];

    return self;
}

兩個 RACChannelTerminal 中包裝的其實是兩個 RACSubject 熱信號,它們既可以作為訂閱者,也可以接收其他對象發送的消息;我們并不希望 leadingSubject 有任何的初始值,但是我們需要 error 和 completed 信息可以被重播。

通過 -ignoreValues 和 -subscribe: 方法, leadingSubject 和 followingSubject 兩個熱信號中產生的錯誤會互相發送,這是為了防止連接的兩端一邊發生了錯誤,另一邊還繼續工作的情況的出現。

在初始化方法的最后,生成兩個 RACChannelTerminal 實例的過程就不多說了。

RACChannel 與 UIKit 組件

如果在整個 ReactiveCocoa 工程中搜索 RACChannel ,你會發現以下的 UIKit 組件都與 RACChannel 有著非常密切的關系:

UIKit 中的這些組件都提供了使用 RACChannel 的接口,用以降低數據雙向綁定的復雜度,我們以 UITextField 為例,它在分類的接口中提供了 rac_newTextChannel 方法:

- (RACChannelTerminal *)rac_newTextChannel {
    return [self rac_channelForControlEvents:UIControlEventAllEditingEvents key:@keypath(self.text) nilValue:@""];
}

上述方法用于返回一個一端綁定 UIControlEventAllEditingEvents 事件的 RACChannelTerminal 對象。

UIControlEventAllEditingEvents 事件發生時,它會將自己的 text 屬性作為信號發送到 followingTerminal -> leadingTerminal 管道中,最后發送給 leadingTerminal 的訂閱者。

在 rac_newTextChannel 中調用的方法 -rac_channelForControlEvents:key:nilValue: 是一個 UIControl 的私有方法:

- (RACChannelTerminal *)rac_channelForControlEvents:(UIControlEvents)controlEvents key:(NSString *)key nilValue:(id)nilValue {
    key = [key copy];
    RACChannel *channel = [[RACChannel alloc] init];

    RACSignal *eventSignal = [[[self
        rac_signalForControlEvents:controlEvents]
        mapReplace:key]
        takeUntil:[[channel.followingTerminal
            ignoreValues]
            catchTo:RACSignal.empty]];
    [[self
        rac_liftSelector:@selector(valueForKey:) withSignals:eventSignal, nil]
        subscribe:channel.followingTerminal];

    RACSignal *valuesSignal = [channel.followingTerminal
        map:^(id value) {
            return value ?: nilValue;
        }];
    [self rac_liftSelector:@selector(setValue:forKey:) withSignals:valuesSignal, [RACSignal return:key], nil];

    return channel.leadingTerminal;
}

這個方法為所有的 UIControl 子類,包括 UITextField 、 UISegmentedControl 等等,它的主要作用就是當傳入的 controlEvents 事件發生時,將 UIKit 組件的屬性 key 發送到返回的 RACChannelTerminal 實例中;同時,在向返回的 RACChannelTerminal 實例中發送消息時,也會自動更新 UIKit 組件的屬性。

上面的代碼在初始化 RACChannel 之后做了兩件事情,首先是在 UIControlEventAllEditingEvents 事件發生時,將 text 屬性發送到 followingTerminal 中:

RACSignal *eventSignal = [[[self  
    rac_signalForControlEvents:controlEvents]
    mapReplace:key]
    takeUntil:[[channel.followingTerminal
        ignoreValues]
        catchTo:RACSignal.empty]];
[[self
    rac_liftSelector:@selector(valueForKey:) withSignals:eventSignal, nil]
    subscribe:channel.followingTerminal];

第二個是在 followingTerminal 接收到來自 leadingTerminal 的消息時,更新 UITextField 的 text 屬性。

RACSignal *valuesSignal = [channel.followingTerminal  
    map:^(id value) {
        return value ?: nilValue;
    }];
[self rac_liftSelector:@selector(setValue:forKey:) withSignals:valuesSignal, [RACSignal return:key], nil];

這兩件事情都是通過 -rac_liftSelector:withSignals: 方法來完成的,不過,我們不會在這篇文章中介紹這個方法。

RACChannel 與 KVO

RACChannel 不僅為 UIKit 組件提供了接口,還為鍵值觀測提供了 RACKVOChannel 來高效地完成雙向綁定; RACKVOChannel 是 RACChannel 的子類:

在 RACKVOChannel 提供的接口中,我們一般都會使用 RACChannelTo 來觀測某一個對象的對應屬性,三個參數依次為對象、屬性和默認值:

RACChannelTerminal *integerChannel = RACChannelTo(self, integerProperty, @42);

而 RACChannelTo 是 RACKVOChannel 頭文件中的一個宏,上面的表達式可以展開成為:

RACChannelTerminal *integerChannel = [[RACKVOChannel alloc] initWithTarget:self keyPath:@"integerProperty" nilValue:@42][@"followingTerminal"];

該宏初始化了一個 RACKVOChannel 對象,并通過方括號的方式獲取其中的 followingTerminal ,這種獲取類屬性的方式是通過覆寫以下的兩個方法實現的:

- (RACChannelTerminal *)objectForKeyedSubscript:(NSString *)key {
    RACChannelTerminal *terminal = [self valueForKey:key];
    return terminal;
}

- (void)setObject:(RACChannelTerminal *)otherTerminal forKeyedSubscript:(NSString *)key {
    RACChannelTerminal *selfTerminal = [self objectForKeyedSubscript:key];
    [otherTerminal subscribe:selfTerminal];
    [[selfTerminal skip:1] subscribe:otherTerminal];
}

又由于覆寫了這兩個方法,在 -setObject:forKeyedSubscript: 時會自動調用 -subscribe: 方法完成雙向綁定,所以我們可以使用 = 來對兩個 RACKVOChannel 進行雙向綁定:

RACChannelTo(view, property) = RACChannelTo(model, property);

[[RACKVOChannel alloc] initWithTarget:view keyPath:@"property" nilValue:nil][@"followingTerminal"] = [[RACKVOChannel alloc] initWithTarget:model keyPath:@"property" nilValue:nil][@"followingTerminal"];

以上的兩種方式是完全等價的,它們都會在對方的屬性更新時更新自己的屬性。

實現的方式其實與 RACChannel 差不多,這里不會深入到代碼中進行介紹,與 RACChannel 的區別是, RACKVOChannel 并沒有暴露出 leadingTerminal 而是 followingTerminal :

RACChannel 實戰

這一小節通過一個簡單的例子來解釋如何使用 RACChannel 進行雙向數據綁定。

在整個視圖上有兩個 UITextField ,我們想讓這兩個 UITextField text 的值相互綁定,在一個 UITextField 編輯時也改變另一個 UITextField 中的內容:

@property (weak, nonatomic) IBOutlet UITextField *textField;
@property (weak, nonatomic) IBOutlet UITextField *anotherTextField;

實現的過程非常簡單,分別獲取兩個 UITextField 的 rac_newTextChannel 屬性,并讓它們訂閱彼此的內容:

[self.textField.rac_newTextChannel subscribe:self.anotherTextField.rac_newTextChannel];
[self.anotherTextField.rac_newTextChannel subscribe:self.textField.rac_newTextChannel];

這樣在使用兩個文本輸入框時就能達到預期的效果了,這是一個非常簡單的例子,可以得到如下的結構圖。

兩個 UITextField 通過 RACChannel 互相影響,在對方屬性更新時同時更新自己的屬性。

總結

RACChannel 非常適合于視圖和模型之間的雙向綁定,在對方的屬性或者狀態更新時及時通知自己,達到預期的效果;我們可以使用 ReactiveCocoa 中內置的很多與 RACChannel 有關的方法,來獲取開箱即用的 RACChannelTerminal ,當然也可以使用 RACChannelTo 通過 RACKVOChannel 來快速綁定類與類的屬性。

References

Github Repo: iOS-Source-Code-Analyze

Follow: Draveness · GitHub

Source: http://draveness.me/racchannel

 

來自:http://draveness.me/racchannel/

 

 本文由用戶 KelChewning 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!