原文 : 與佳期的個人博客(gonghonglou.com)
ReactiveCocoa 是一個Objective-C 框架,受 Functional Reactive Programming的啟發(fā)瞳腌。它提供了一系列用來組合和轉(zhuǎn)換值流的API呜投。
如果你早已熟悉了函數(shù)響應(yīng)式編程或者知道ReactiveCocoa的基本前提葫隙,看看Documentation這個文件夾里的framework overview等文件來了解它是怎樣在實踐中工作的统屈。
介紹
ReactiveCocoa受functional reactive programming的啟發(fā)。在那些能被替換和修改的地方撑毛,RAC提供信號(由RACSignal
代表)來捕獲當前和將來的值而不是使用可變的變量书聚。
一個文本框能夠根據(jù)它的改變被綁定到最后一次的值,而不是使用額外的代碼每秒去監(jiān)控時鐘和更新文本框藻雌。這點跟KVO很像雌续,不過使用了block,而不是-observeValueForKeyPath:ofObject:change:context:
信號也可以進行異步操作胯杭,就像futures and promises驯杜。這極大的簡化了異步軟件中網(wǎng)絡(luò)連接的代碼。
RAC的重大優(yōu)勢之一就是它提供信號(signal
)這種方式來統(tǒng)一的處理所有異步的行為做个,包括代理方法鸽心、block 回調(diào)、target-action 機制居暖、通知和KVO顽频。
這里是簡單的例子:
// 當self.username改變時,打印新的名字到控制臺
//
// RACObserve(self, username)創(chuàng)建一個新的RACSignal太闺,當前self.username的值發(fā)生改變時糯景,發(fā)送新值給newName
// -subscribeNext: 當信號發(fā)送值時將觸發(fā)block
[RACObserve(self, username) subscribeNext:^(NSString *newName) {
NSLog(@"%@", newName);
}];
與KVO 通知不同的是信號能夠進行統(tǒng)一的鏈式操作:
// 只有當名字的開頭為"j"時才打印
//
// -filter 只有當block返回YES時才會創(chuàng)建一個新的RACSignal發(fā)送一個新值
[[RACObserve(self, username)
filter:^(NSString *newName) {
return [newName hasPrefix:@"j"];
}]
subscribeNext:^(NSString *newName) {
NSLog(@"%@", newName);
}];
信號也能被用來派生狀態(tài)。在響應(yīng)新值中RAC代替觀察屬性和設(shè)置其他的屬性跟束,能夠在信號和運行周期內(nèi)傳達屬性:
// 當self.password 和 self.passwordConfirmation相同時創(chuàng)建一個單向的binding使得self.createEnabled為true
//
// RAC() 是一個宏指令使得binding看起來nicer
//
// +combineLatest:reduce: 建一個信號數(shù)組
// 當任一個信號的最后一個值發(fā)生改變時觸發(fā)這個block莺奸,返回一個新的RACSignal,將block返回的值作為values發(fā)送出去
RAC(self, createEnabled) = [RACSignal
combineLatest:@[ RACObserve(self, password), RACObserve(self, passwordConfirmation) ]
reduce:^(NSString *password, NSString *passwordConfirm) {
return @([passwordConfirm isEqualToString:password]);
}];
信號不僅是在KVO上冀宴,還能在建立在隨著時間而改變的值流上灭贷。例如,它們可以代表按鈕點擊:
// 當按鈕被點擊時打印信息
//
// RACCommand創(chuàng)建信號去表示UI行為略贮。例如甚疟,每一個信號可以表示一個按鈕的點擊仗岖、與它相關(guān)聯(lián)的附加工作
//
// -rac_command是封裝的NSButton方法. 當按鈕被點擊時將發(fā)送到該命令
self.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id _) {
NSLog(@"button was pressed!");
return [RACSignal empty];
}];
或者是異步網(wǎng)絡(luò)操作:
// 連接"Log in"按鈕給網(wǎng)絡(luò)登錄
//
// 當?shù)卿浢顖?zhí)行時運行block,開始登錄進度
self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^(id sender) {
// 假設(shè)當網(wǎng)絡(luò)請求完成時 -logIn 方法返回一個信號發(fā)送一個value
return [client logIn];
}];
// -executionSignals 每次執(zhí)行該命令時览妖,這個方法返回一個信號轧拄,包括以前的block返回的信號
[self.loginCommand.executionSignals subscribeNext:^(RACSignal *loginSignal) {
// 成功登錄時打印信息
[loginSignal subscribeCompleted:^{
NSLog(@"Logged in successfully!");
}];
}];
// 按鈕被點擊時執(zhí)行登錄命令
self.loginButton.rac_command = self.loginCommand;
信號也可以代表定時器,其他的UI事件讽膏,或者別的什么隨時間而改變的事件檩电。
在異步操作方面,通過鏈接和轉(zhuǎn)換信號可以建立更復(fù)雜的行為府树。在一組完整的操作之后更簡單的來執(zhí)行工作:
// 執(zhí)行2個網(wǎng)絡(luò)操作俐末,當它們都完成時打印信息到控制臺
//
// +merge: 當數(shù)組里的所有信號完成時,返回一個新的RACSignal
//
// -subscribeCompleted: 當信號完成時將執(zhí)行這個block
[[RACSignal
merge:@[ [client fetchUserRepos], [client fetchOrgRepos] ]]
subscribeCompleted:^{
NSLog(@"They're both done!");
}];
信號可以被鏈接到順序執(zhí)行異步操作奄侠,而不是使用一堆block回調(diào)卓箫。通常這樣簡單的來使用futures and promises:
// 用戶登錄,下載緩存信息垄潮,獲取服務(wù)器信息烹卒。都完成后將信息打印到控制臺
//
// 假設(shè)登錄之后 -logInUser 方法返回一個信號
//
// -flattenMap: 當信號發(fā)送一個value時觸發(fā)這個block
// 并且返回一個新的RACSignal來整合從block返回的所有的信號到一個單一信號中
[[[[client
logInUser]
flattenMap:^(User *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甚至可以簡單的建立在一個異步操作的結(jié)果上:
// 創(chuàng)建一個單向的binding弯洗,讓 self.imageView.image 來放置下載下來的user的頭像
//
// 假設(shè) -fetchUserWithUsername: 方法返回一個信號發(fā)送給user
//
// -deliverOn: 創(chuàng)建新的信號在其他的隊列中進行他們的工作
// 在這個例子中旅急,此方法被用來將工作轉(zhuǎn)移到后臺隊列和回到主線程
//
// -map: 每個user調(diào)用這個block,獲取并且返回一個新的RACSignal牡整,并且將從block返回的值發(fā)送出去
RAC(self.imageView, image) = [[[[client
fetchUserWithUsername:@"joshaber"]
deliverOn:[RACScheduler scheduler]]
map:^(User *user) {
// 下載頭像 (在后臺隊列中進行).
return [[NSImage alloc] initWithContentsOfURL:user.avatarURL];
}]
// 此時這個任務(wù)將在主線程中執(zhí)行
deliverOn:RACScheduler.mainThreadScheduler];
這是一些使用RAC的示范操作坠非,但是它并不能說明RAC為什么如此強大。
更多示例代碼參見C-41 或 GroceryList,這些是使用ReactiveCocoa編寫的iOS APP果正。在這個文件夾Documentation中可以查到更多的關(guān)于RAC的信息。
使用ReactiveCocoa
乍一看ReactiveCocoa是非常抽象的盟迟,很難理解該怎樣將它應(yīng)用到具體的問題上秋泳。
這有一些示例來展示RAC的優(yōu)勢
處理異步或事件驅(qū)動的數(shù)據(jù)源
許多Cocoa編程的重點是對用戶事件的反應(yīng)或應(yīng)用狀態(tài)的變化。處理這些事件的代碼很快變得非常復(fù)雜的就像意大利面一樣攒菠,伴隨著許多回調(diào)函數(shù)和狀態(tài)變量處理順序的問題迫皱。
表面上看起來模式不同,比如UI回調(diào)辖众,網(wǎng)絡(luò)響應(yīng)和KVO通知卓起,實際上有很多共同之處。RACSignal統(tǒng)一了所有的這些不同的API凹炸,使他們可以組合在一起戏阅,并以同樣的方式操縱。
例如這樣的代碼:
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];
}
鏈接依賴操作
依賴在網(wǎng)絡(luò)請求中是常見的啤它,在下一個請求建立之前奕筐,需要完成當前對服務(wù)器的請求舱痘,比如:
[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中可以這樣簡單的實現(xiàn):
[[[[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.");
}];
并行獨立工作
與獨立的數(shù)據(jù)集合并行工作,然后將它們合并成一個non-trivial函數(shù)到Cocoa离赫,并經(jīng)常涉及大量的同步:
objc
__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];
上面的代碼可以用簡單的合成信號來清理和優(yōu)化:
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");
}];
簡化collection轉(zhuǎn)換
高亮命令函數(shù)比如 map
, filter
, fold
/reduce
在Foundation中是非常缺少的芭逝,導(dǎo)致循環(huán)中的代碼像這樣:
NSMutableArray *results = [NSMutableArray array];
for (NSString *str in strings) {
if (str.length < 2) {
continue;
}
NSString *newString = [str stringByAppendingString:@"foobar"];
[results addObject:newString];
}
RACSequence允許所有Cocoa collection在統(tǒng)一的和聲明的方式下被操作:
RACSequence *results = [[strings.rac_sequence
filter:^ BOOL (NSString *str) {
return str.length >= 2;
}]
map:^(NSString *str) {
return [str stringByAppendingString:@"foobar"];
}];
后記
以上文章摘譯自ReactiveCocoa的Objective-C官方文檔ReactiveCocoa Documentation
以上內(nèi)容介紹了RAC的基本用法,僅限于使用渊胸,所以墻裂建議仔細學(xué)習(xí)下節(jié)的RAC學(xué)習(xí)資料旬盯,了解RAC原理及高階用法。
小白出手翎猛,請多指教胖翰。如言有誤,還望斧正办成!
轉(zhuǎn)載請保留原文地址http://gonghonglou.com/2016/03/17/meet-ReactiveCocoa
RAC學(xué)習(xí)資料
- ReactiveCocoa的Objective-C官方文檔ReactiveCocoa Documentation
- 雷純鋒的ReactiveCocoa v2.5 源碼解析之架構(gòu)總覽
- 吖了個崢的最快讓你上手ReactiveCocoa之基礎(chǔ)篇 和 最快讓你上手ReactiveCocoa之進階篇
- 李忠的ReactiveCocoa與Functional Reactive Programming
- 唐巧的ReactiveCocoa 討論會