簡介
- 項目主頁: ReactiveCocoa
- 實例下載: https://github.com/ios122/ios122
- 簡評: 最受歡迎,最有價值的iOS響應式編程庫,沒有之一!iOS MVVM模式的完美搭檔,更多關於MVVM與ReactiveCocoa的討論,參考這篇文章: 【長篇高能】ReactiveCocoa 和 MVVM 入門
- 注意: ReactiveCocoa 最新3.0版本,使用Swift重寫,最低支持iOS8.0,與國內大多數公司實際現狀(一般要求最低兼容iOS7.0)不符;故此處選擇兼容性版本更低的 2.5 版本來進行對譯與解讀.
系統要求
- iOS 7.0 + (ReactiveCocoa 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"];
}];