標籤:http ict html objects prefix sse ref 響應式 ios 7
ReactiveCocoa,最受歡迎的iOS函數響應式編程庫(2.5版),沒有之一!簡介
項目首頁: ReactiveCocoa
執行個體下載: https://github.com/ios122/ios122
簡評: 最受歡迎,最有價值的iOS響應式編程庫,沒有之一!iOS MVVM模式的完美搭檔,更多關於MVVM與ReactiveCocoa的討論,參考這篇文章: 【長篇高能】ReactiveCocoa 和 MVVM 入門
注意: ReactiveCocoa 最新3.0版本,使用Swift重寫,最低支援iOS8.0,與國內大多數公司實際現狀(一般要求最低相容iOS7.0)不符;故此處選擇相容性版本更低的 2.5 版本來進行對譯與解讀.
系統要求
安裝
推薦使用 CocoaPods 安裝:
platform :ios, ‘7.0‘pod "ReactiveCocoa" # RAC,一個支援響應式編程的庫.
入門
ReactiveCocoa 靈感來源於 函數響應式編程. ReactiveCocoa通常簡稱為RAC.RAC中,不再使用變數,而是使用訊號(以 RACSignal
為代表)來捕捉現在和未來的資料或視圖的值.
通過對訊號的連結,組合與響應, 軟體就可以聲明式的方式書寫;這樣就不再需要頻繁地去監測和更新資料或視圖的值了.
RAC 主要特性之一就是提供了一種單一又統一的方式來處理各種非同步作業--包括代理方法,block回調,target-action機制,通知和KVO等.
這是一個簡單的例子:
// 當self.username變化時,在控制台列印新的名字.//// RACObserve(self, username) 建立一個新的 RACSignal 訊號對象,它將會發送self.username當前的值,和以後 self.username 發生變化時 的所有新值.// -subscribeNext: 無論signal訊號對象何時發送訊息,此block回調都將會被執行.[RACObserve(self, username) subscribeNext:^(NSString *newName) { NSLog(@"%@", newName);}];
但是和KVO不同的是, signals訊號對象支援鏈式操作:
// 只列印以"j"開頭的名字.//// -filter: 當其bock方法返回YES時,才會返回一個新的RACSignal 訊號對象;即如果其block方法返回NO,訊號不再繼續往下傳播.[[RACObserve(self, username) filter:^(NSString *newName) { return [newName hasPrefix:@"j"]; }] subscribeNext:^(NSString *newName) { NSLog(@"%@", newName); }];
Signals訊號也可以用於派生屬性(即那些由其他屬性的值決定的屬性,如Person可能有一個屬性為 age年齡 和一個屬性 isYong是否年輕,isYong 是由 age 屬性的值推斷而來,由age本身的值決定).不再需要來監測某個屬性的值,然後來對應更新其他受此屬性的新值影響的屬性的值.RAC 可以支援以signales訊號和操作的方式來表達派生屬性.
// 建立一個單向綁定, self.password和self.passwordConfirmation 相等// 時,self.createEnabled 會自動變為true.//// RAC() 是一個宏,使綁定看起來更NICE.// // +combineLatest:reduce: 使用一個 signals 訊號的數組;// 在任意signal變化時,使用他們的最後一次的值來執行block;// 並返回一個新的 RACSignal訊號對象來將block的值用作屬性的新值來發送;// 簡單說,類似於重寫createEnabled 屬性的 getter 方法.RAC(self, createEnabled) = [RACSignal combineLatest:@[ RACObserve(self, password), RACObserve(self, passwordConfirmation) ] reduce:^(NSString *password, NSString *passwordConfirm) { return @([passwordConfirm isEqualToString:password]); }];// 使用時,是不需要考慮屬性是否是派生屬性以及以何種方式綁定的:[RACObserve(self, createEnabled) subscribeNext: ^(NSNumber * enbable){ NSLog(@"%@", enbable);}];
Signals訊號可以基於任何隨時間變化的資料流建立,不僅僅是KVO.例如說,他們可以用來表示一個按鈕的點擊事件:
// 任意時間點擊按鈕,都會列印一條訊息. //// RACCommand 建立代表UI事件的signals訊號.例如,單個訊號都可以代表一個按鈕被點擊,// 然後會有一些額外的操作與處理.//// -rac_command 是NSButton的一個擴充.按鈕被點擊時,會將會把自身發送給rac_command self.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id _) { NSLog(@"button was pressed!"); return [RACSignal empty];}];
或者非同步網路請求:
// 監聽"登陸"按鈕,並記錄網路請求成功的訊息.// 這個block會在來任意開始登陸步驟,執行登陸命令時調用.self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^(id sender) { // 這是一個假想中的 -logIn 方法, 返回一個 signal訊號對象,這個對象在網路對象完成時發送 值. // 可以使用 -filter 方法來保證若且唯若網路請求完成時,才返回一個 signal 對象. return [client logIn];}];// -executionSignals 返回一個signal對象,這個signal對象就是每次執行命令時通過上面的block返回的那個signal對象.[self.loginCommand.executionSignals subscribeNext:^(RACSignal *loginSignal) { // 列印資訊,不論何時我們登陸成功. [loginSignal subscribeCompleted:^{ NSLog(@"Logged in successfully!"); }];}];// 當按鈕被點擊時,執行login命令.self.loginButton.rac_command = self.loginCommand;
Signals訊號 也可以表示定時器,其他的UI事件,或者任何其他會隨時間變化的東西.
在非同步作業上使用signals訊號,讓通過連結和轉換這些signal訊號,構建更加複雜的行為成為可能.可以在一組操作完成後,來觸發此操作即可:
// 執行兩個網路操作,並在它們都完成後在控制台列印資訊.//// +merge: 傳入一組signal訊號,並返回一個新的RACSignal訊號對象.這個新返回的RACSignal訊號對象,傳遞所有請求的值,並在所有的請求完成時完成.即:新返回的RACSignal訊號,在每個請求完成時,都會發送個訊息;在所有訊息完成時,除了發送訊息外,還會觸發"完成"相關的block.//// -subscribeCompleted: signal訊號完成時,將會執行block.[[RACSignal merge:@[ [client fetchUserRepos], [client fetchOrgRepos] ]] subscribeCompleted:^{ NSLog(@"They‘re both done!"); }];
Signals 訊號可以被連結以連續執行非同步作業,而不再需要嵌套式的block調用.用法類似於:
// 使用者登入,然後載入緩衝資訊,然後從伺服器擷取剩餘的訊息.在這一切完成後,輸入資訊到控制台.//// 假想的 -logInUser 方法,在登入完成後,返回一個signal訊號對象.//// -flattenMap: 無論任何時候,signal訊號發送一個值,它的block都將被執行,然後返回一個新的RACSignal,這個新的RACSignal訊號對象會merge合并所有此block返回的signals訊號為一個RACSignal訊號對象.[[[[client logInUser] flattenMap:^(User *user) { // Return a signal that loads cached messages for the user. return [client loadCachedMessagesForUser:user]; }] flattenMap:^(NSArray *messages) { // Return a signal that fetches any remaining messages. return [client fetchMessagesAfterMessage:messages.lastObject]; }] subscribeNext:^(NSArray *newMessages) { NSLog(@"New messages: %@", newMessages); } completed:^{ NSLog(@"Fetched all messages."); }];
RAC 甚至讓綁定非同步作業的結果也更容易:
// 建立一個單向的綁定,遮掩self.imagView.image就可以在使用者的頭像下載完成後自動被設定.//// 假定的 -fetchUserWithUsername: 方法返回一個發送使用者物件的signal訊號對象.//// -deliverOn: 建立一個新的 signals 訊號對象,以在其他隊列來處理他們的任務.// 在這個樣本中,這個方法被用來將任務移到後台隊列,並在稍後下載完成後返回主線程中.//// -map: 每個擷取的使用者都會傳遞進到這個block,然後返回新的RACSignal訊號對象,這個// signal訊號對象發送從這個block返回的值.RAC(self.imageView, image) = [[[[client fetchUserWithUsername:@"joshaber"] deliverOn:[RACScheduler scheduler]] map:^(User *user) { // 下載頭像(這在後台執行.) return [UIImage imageWithData: [NSData dataWithContentsOfURL: user.avatarURL]]; }] // 現在賦值在主線程完成. deliverOn:RACScheduler.mainThreadScheduler];
何時使用 ReactiveCocoa ?
ReactiveCocoa 非常抽象,初次接觸,通常很難理解如何使用它來解決具體的問題.
這是一些使用RAC更具有優勢的應用情境:
處理非同步或事件驅動的資料來源.
大多說Cocoa程式的重心在於響應使用者事件或程式狀態的變化上.處理這些情況的代碼,很快就會變得很複雜,就像意大利麵條那樣,擁有許多的回調和狀態變數來處理順序問題.
一些編程模式,表面上看有些相似,比如 UI回調方法,網路請求的響應和KVO通知等;實際上他們擁有許多的共同點. RACSignal 訊號類,統一類這些不同的APIS,以便組合使用和操作它們.
例如,如下代碼:
static void *ObservationContext = &ObservationContext;- (void)viewDidLoad { [super viewDidLoad]; [LoginManager.sharedManager addObserver:self forKeyPath:@"loggingIn" options:NSKeyValueObservingOptionInitial context:&ObservationContext]; [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(loggedOut:) name:UserDidLogOutNotification object:LoginManager.sharedManager]; [self.usernameTextField addTarget:self action:@selector(updateLogInButton) forControlEvents:UIControlEventEditingChanged]; [self.passwordTextField addTarget:self action:@selector(updateLogInButton) forControlEvents:UIControlEventEditingChanged]; [self.logInButton addTarget:self action:@selector(logInPressed:) forControlEvents:UIControlEventTouchUpInside];}- (void)dealloc { [LoginManager.sharedManager removeObserver:self forKeyPath:@"loggingIn" context:ObservationContext]; [NSNotificationCenter.defaultCenter removeObserver:self];}- (void)updateLogInButton { BOOL textFieldsNonEmpty = self.usernameTextField.text.length > 0 && self.passwordTextField.text.length > 0; BOOL readyToLogIn = !LoginManager.sharedManager.isLoggingIn && !self.loggedIn; self.logInButton.enabled = textFieldsNonEmpty && readyToLogIn;}- (IBAction)logInPressed:(UIButton *)sender { [[LoginManager sharedManager] logInWithUsername:self.usernameTextField.text password:self.passwordTextField.text success:^{ self.loggedIn = YES; } failure:^(NSError *error) { [self presentError:error]; }];}- (void)loggedOut:(NSNotification *)notification { self.loggedIn = NO;}- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == ObservationContext) { [self updateLogInButton]; } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; }}
… 可以用RAC這樣重寫:
- (void)viewDidLoad { [super viewDidLoad]; @weakify(self); RAC(self.logInButton, enabled) = [RACSignal combineLatest:@[ self.usernameTextField.rac_textSignal, self.passwordTextField.rac_textSignal, RACObserve(LoginManager.sharedManager, loggingIn), RACObserve(self, loggedIn) ] reduce:^(NSString *username, NSString *password, NSNumber *loggingIn, NSNumber *loggedIn) { return @(username.length > 0 && password.length > 0 && !loggingIn.boolValue && !loggedIn.boolValue); }]; [[self.logInButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *sender) { @strongify(self); RACSignal *loginSignal = [LoginManager.sharedManager logInWithUsername:self.usernameTextField.text password:self.passwordTextField.text]; [loginSignal subscribeError:^(NSError *error) { @strongify(self); [self presentError:error]; } completed:^{ @strongify(self); self.loggedIn = YES; }]; }]; RAC(self, loggedIn) = [[NSNotificationCenter.defaultCenter rac_addObserverForName:UserDidLogOutNotification object:nil] mapReplace:@NO];}
鏈式依賴的操作.
依賴關係通常出現在網路請求中,如後一個請求應該等前一個請求完成後再建立,等等:
[client logInWithSuccess:^{ [client loadCachedMessagesWithSuccess:^(NSArray *messages) { [client fetchMessagesAfterMessage:messages.lastObject success:^(NSArray *nextMessages) { NSLog(@"Fetched all messages."); } failure:^(NSError *error) { [self presentError:error]; }]; } failure:^(NSError *error) { [self presentError:error]; }];} failure:^(NSError *error) { [self presentError:error];}];
ReactiveCocoa 可以特別方便地處理這種邏輯模式:
[[[[client logIn] then:^{ return [client loadCachedMessages]; }] flattenMap:^(NSArray *messages) { return [client fetchMessagesAfterMessage:messages.lastObject]; }] subscribeError:^(NSError *error) { [self presentError:error]; } completed:^{ NSLog(@"Fetched all messages."); }];
並行獨立的工作.
使用獨立資料的並行工作,然後最終將他們合并到一個結果中,在Cocoa中是很瑣碎的,並且常常包含許多同步代碼:
__block NSArray *databaseObjects;__block NSArray *fileContents; NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc] init];NSBlockOperation *databaseOperation = [NSBlockOperation blockOperationWithBlock:^{ databaseObjects = [databaseClient fetchObjectsMatchingPredicate:predicate];}];NSBlockOperation *filesOperation = [NSBlockOperation blockOperationWithBlock:^{ NSMutableArray *filesInProgress = [NSMutableArray array]; for (NSString *path in files) { [filesInProgress addObject:[NSData dataWithContentsOfFile:path]]; } fileContents = [filesInProgress copy];}]; NSBlockOperation *finishOperation = [NSBlockOperation blockOperationWithBlock:^{ [self finishProcessingDatabaseObjects:databaseObjects fileContents:fileContents]; NSLog(@"Done processing");}]; [finishOperation addDependency:databaseOperation];[finishOperation addDependency:filesOperation];[backgroundQueue addOperation:databaseOperation];[backgroundQueue addOperation:filesOperation];[backgroundQueue addOperation:finishOperation];
以上代碼可以通過複合使用signals訊號對象來最佳化:
RACSignal *databaseSignal = [[databaseClient fetchObjectsMatchingPredicate:predicate] subscribeOn:[RACScheduler scheduler]];RACSignal *fileSignal = [RACSignal startEagerlyWithScheduler:[RACScheduler scheduler] block:^(id<RACSubscriber> subscriber) { NSMutableArray *filesInProgress = [NSMutableArray array]; for (NSString *path in files) { [filesInProgress addObject:[NSData dataWithContentsOfFile:path]]; } [subscriber sendNext:[filesInProgress copy]]; [subscriber sendCompleted];}];[[RACSignal combineLatest:@[ databaseSignal, fileSignal ] reduce:^ id (NSArray *databaseObjects, NSArray *fileContents) { [self finishProcessingDatabaseObjects:databaseObjects fileContents:fileContents]; return nil; }] subscribeCompleted:^{ NSLog(@"Done processing"); }];
簡化集合的轉換.
更高層級的排序函數,比如 map
(映射), filter
(過濾器), fold
(摺疊)/reduce
(減少),在Foundation 中嚴重缺失; 這導致必須編寫類似於下面的迴圈代碼:
NSMutableArray *results = [NSMutableArray array];for (NSString *str in strings) { if (str.length < 2) { continue; } NSString *newString = [str stringByAppendingString:@"foobar"]; [results addObject:newString];}
RACSequence 允許任何Cocoa集合可以使用統一的聲明式文法來操作:
RACSequence *results = [[strings.rac_sequence filter:^ BOOL (NSString *str) { return str.length >= 2; }] map:^(NSString *str) { return [str stringByAppendingString:@"foobar"]; }];
ReactiveCocoa,最受歡迎的iOS函數響應式編程庫(2.5版),沒有之一!