【译文练习】ReactiveCocoa概要:了解和使用RACCommand
原文地址
點擊這里
這幾天部門的前輩再用RAC的時候問到一個問題,RACCommand在RAC中具體的作用和起到的功能,到底應該如何應用它。
關于RAC的使用文章非常多,但是大多僅限于介紹和基本的使用方法,很少介紹RAC究竟應該如何優雅的嵌入到項目中。
在查閱資料的時候發現了此篇博文,寫的非常細致,所以做了一次搬運工。
另,妹子我的英文屬于渣渣系列,所以有什么翻譯不當,請一定要指教。
Code
文章中所有代碼在這里
RACCommand是你的新伙伴嗎?
RACCommand是ReactiveCocoa最精華的部分之一,它可以讓你在開發中節約大量的時間并讓你的iOS或者OS X app有更強的魯棒性。
我見過不少剛接觸ReactiveCocoa(后文將簡寫為RAC),還不能完全理解RACCommand是如何工作又不知何時應該使用RACCommand的同學。所以我認為這個小介紹將會很實用,可以給他們帶來一些啟發。官方文檔并沒有給出多少如何使用RACCommand的Examples,但是RACCommand頭文件的介紹還是很不錯的,不過這對剛開始用RAC的同學來說還是太難理解了。
RACCommand類是用于表示一些操作的執行。通常,是由于UI上的一些事件觸發了RACCommand的執行。比如當用戶按了一個按鈕,如果對應RACCommand實例可以被執行,就會執行相應的操作。這使得它很容易和UI進行綁定,同時可以保證當RACCommand處于not enabled時RACCommand實例的操作不會被執行。當Command可以執行時,常做的方式是把allowsconcuuent的屬性設置為NO,這可以保證Command已經執行完成后不會被重復執行。Command執行的結果是一個RACSignal,因此你可以調用next:、completed:、或者error:。后面將會展示具體使用方式。
Example App
我們假設我們正在設計一個簡單的app,其功能是讓用戶訂閱一個郵件。最簡單的方式是,用一個UITextField和一個UIButton。當用戶輸入email并且點擊按鈕的時候,email地址將會傳給某個web服務。看起來很簡單,但是我們應該確保用戶有最好的體驗。如果用戶按了兩次按鈕?``如何處理請求出錯?``如果email不合法?``RACCommand可以幫助我們處理這些情況。在這篇文章中將一步步完善這個小app以此來討論一些概念和工作原理。
可以從這里獲得源碼。
從一個非常簡單的ViewController可以很好的實踐MVVM模式。
- (void)bindWithViewModel {RAC(self.viewModel, email) = self.emailTextField.rac_textSignal;self.subscribeButton.rac_command = self.viewModel.subscribeCommand;RAC(self.statusLabel, text) = RACObserve(self.viewModel, statusMessage); } 復制代碼在上面的方法(在viewDidLoad中調用),在View和ViewModel中建立了綁定關系。下面是ViewModel的定義:
@interface SubscribeViewModel : NSObject@property(nonatomic, strong) RACCommand *subscribeCommand;// write to this property @property(nonatomic, strong) NSString *email;// read from this property @property(nonatomic, strong) NSString *statusMessage;@end 復制代碼如上所示,一個暴露出的RACCommand屬性。另外兩個是字符串屬性,它們和View的兩個屬性綁定在一起。ViewModel的完整實現如下:
static NSString *const kSubscribeURL = @"http://reactivetest.apiary.io/subscribers";@interface SubscribeViewModel () @property(nonatomic, strong) RACSignal *emailValidSignal; @end@implementation SubscribeViewModel- (id)init {self = [super init];if (self) {[self mapSubscribeCommandStateToStatusMessage];}return self; }- (void)mapSubscribeCommandStateToStatusMessage {RACSignal *startedMessageSource = [self.subscribeCommand.executionSignals map:^id(RACSignal *subscribeSignal) {return NSLocalizedString(@"Sending request...", nil);}];RACSignal *completedMessageSource = [self.subscribeCommand.executionSignals flattenMap:^RACStream *(RACSignal *subscribeSignal) {return [[[subscribeSignal materialize] filter:^BOOL(RACEvent *event) {return event.eventType == RACEventTypeCompleted;}] map:^id(id value) {return NSLocalizedString(@"Thanks", nil);}];}];RACSignal *failedMessageSource = [[self.subscribeCommand.errors subscribeOn:[RACScheduler mainThreadScheduler]] map:^id(NSError *error) {return NSLocalizedString(@"Error :(", nil);}];RAC(self, statusMessage) = [RACSignal merge:@[startedMessageSource, completedMessageSource, failedMessageSource]]; }- (RACCommand *)subscribeCommand {if (!_subscribeCommand) {NSString *email = self.email;_subscribeCommand = [[RACCommand alloc] initWithEnabled:self.emailValidSignal signalBlock:^RACSignal *(id input) {return [SubscribeViewModel postEmail:email];}];}return _subscribeCommand; }+ (RACSignal *)postEmail:(NSString *)email {AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];manager.requestSerializer = [AFJSONRequestSerializer new];NSDictionary *body = @{@"email": email ?: @""};return [[[manager rac_POST:kSubscribeURL parameters:body] logError] replayLazily]; }- (RACSignal *)emailValidSignal {if (!_emailValidSignal) {_emailValidSignal = [RACObserve(self, email) map:^id(NSString *email) {return @([email isValidEmail]);}];}return _emailValidSignal; }@end 復制代碼這看起來真是一大坨~看還是從小的地方來看吧。我們真正感興趣RACCommandRACCommand創建部分是以下代碼:
- (RACCommand *)subscribeCommand {if (!_subscribeCommand) {NSString *email = self.email;_subscribeCommand = [[RACCommand alloc] initWithEnabled:self.emailValidSignal signalBlock:^RACSignal *(id input) {return [SubscribeViewModel postEmail:email];}];}return _subscribeCommand; } 復制代碼Command通過一個enabledSignal參數來初始化。這個Signal可以指示Command是否可以被執行。在我們本次的用例中Command應該在用戶合法輸入email時允許被執行。self.emailValidSignal就是用來在email發生變化發送NO或者YES指示的。
signalBlock參數在Command需要執行時被調用。block應該返回一個signal。當我們設置allowsConcurrentExecution為NO,Command將會看守這個signal并且在本次執行未完成前不允許任何新的執行。
由于本次用例中的Command來自于按鈕的rac_command(在UIButtton+RACCommandSupport分類中定義),根據Command是否可以被執行,按鈕會自動切換enabled和disabled狀態。
當然,Command會在按鈕被用戶點擊的時候自動執行。我們可以通過RACCommand自由的實現這一切。如果你需要手動執行你可以調用-[RACCommand execute:],參數是可選的,你可以傳遞nil。我們的用例里不需要參數,不過這里的參數通常會十分有用(按鈕可以將自己當做-execute:的參數傳入)。-execute:方法也是一個你可以監控執行狀態的地方,你可以這樣寫:
[[self.viewModel.subscribeCommand execute:nil] subscribeCompleted:^{NSLog(@"The command executed"); }]; 復制代碼在我們的用例中按鈕為我們調用Command的執行(所以我們不需要手動調用-execute:),所以在Command執行時,為了及時更新UI,我們需要監聽Command的另一個屬性。有幾個讓人迷惑的地方,RACCommand的executionSignals屬性是一個每當Commands開始執行時就發送next:的Signal。問題在于Signal由Command創建,所以Signal中還有一層Signal。每次Command開始執行的時候, 我們會在ViewModel中通過mapSubscribeCommandStateToStatusMessage方法里面獲取到一個信號。同時在這個信號里面返回了一個字符串:
RACSignal *startedMessageSource = [self.subscribeCommand.executionSignals map:^id(RACSignal *subscribeSignal) {return NSLocalizedString(@"Sending request...", nil); }]; 復制代碼假如我們想用更函數的方式,來在Command執行完成后都能獲取string,我們需要做更多的工作:
RACSignal *completedMessageSource = [self.subscribeCommand.executionSignals flattenMap:^RACStream *(RACSignal *subscribeSignal) {return [[[subscribeSignal materialize] filter:^BOOL(RACEvent *event) {return event.eventType == RACEventTypeCompleted;}] map:^id(id value) {return NSLocalizedString(@"Thanks", nil);}]; }]; 復制代碼當Command執行時,flattenMap:方法調用一個帶subscribeSignal參數的block。這個block返回一個新的Signal并且它的值會被傳遞到下一個返回信號。materialize操作符讓我們捕獲到一個RACEvent(例如 next: complete 和 error:都是RACEvent的實例)。我們可以在信號完成之后過濾這些event并且映射成一個string。這些解釋讓你暈了嗎,不過你可以去看一下flattenMap:和materialize的文檔以助于你的理解。
我們可以用另一種不同但更容易理解的方式來實現:
@weakify(self); [self.subscribeCommand.executionSignals subscribeNext:^(RACSignal *subscribeSignal) {[subscribeSignal subscribeCompleted:^{@strongify(self);self.statusMessage = @"Thanks";}]; }]; 復制代碼但是我并不喜歡上面的寫法,因為這樣會block中的操作會更多并且會更多的在block中使用到self。所以在這里還使用了@weakify和@strongify(在libextobjc中定義)避免循環retain。
關于executionSignals屬性,有一個重要的細節。在這里的Signal所發送的event不包含error,所以對于那些有特殊errors屬性
RACSignal *failedMessageSource = [[self.subscribeCommand.errors subscribeOn:[RACScheduler mainThreadScheduler]] map:^id(NSError *error) {return NSLocalizedString(@"Error :(", nil); }]; 復制代碼如果我們有三個帶有狀態消息的Signal,我們可以將他們合并成一個信號并綁定到ViewModel的一個statusMessage屬性 (statusMessage綁定ViewController的statusLabel.text)。
RAC(self, statusMessage) = [RACSignal merge:@[startedMessageSource, completedMessageSource, failedMessageSource]]; 復制代碼那么以上是一個RACCommand在iOS app 開發中的一個example。我相信這種實現邏輯比使用UITextFieldDelegate有更多的優點,能在屬性和變量中體現更多的狀態。
其他有趣的RACCommand使用細節
RACCommand有一個executing屬性,實際上它是一個當execute:時會發送YES,終止時發送NO的信號。在訂閱信號時這個信號將會發送它的當前值,如果你只需要獲取當前值而不需要獲得信號,你可以通過以下方式:
BOOL commandIsExecuting = [[command.executing first] boolValue]; 復制代碼enabled屬性也是一個發送YES和NO的信號。當Command通過發送NO的enabledSignal信號創建,或者如果信號在執行并且allowsConcurrentExecutions 為 NO,enabled就會發送NO。
-execute:方法會自動訂閱原始Signal并且廣播它。這意味著你不需要去訂閱-execute:返回的信號,但是如果你訂閱了也不需要擔心它會被執行兩次。
有什么問題都可以在博文后面留言,或者微博上私信我,或者郵件我 coderfish@163.com。
博主是 iOS 妹子一枚。
希望大家一起進步。
我的微博:小魚周凌宇
總結
以上是生活随笔為你收集整理的【译文练习】ReactiveCocoa概要:了解和使用RACCommand的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux文本查看命令之cat
- 下一篇: 阿里云获全球第一张云安全国际认证金牌