介紹
ReactiveObjC的靈感來自函數(shù)式響應(yīng)式編程。RAC使用了捕獲當(dāng)前值和未來值的信號(hào)(RACSignal),而沒有使用替換和修改變量值的方式。
通過鏈接、組合和對(duì)信號(hào)響應(yīng)的方式以聲明的方式編寫程序。
例如,一個(gè)輸入框可以獲取最新的時(shí)間,即使發(fā)什了改變,也不用使用額外的代碼來監(jiān)聽時(shí)間和每秒更新文本內(nèi)容。它的工作原理很像KVO,但是是使用了block代替-observeValueForKeyPath:ofObject:change:context:方法。
信號(hào)也可以用來實(shí)現(xiàn)異步操作,就像futures和promises一樣。這大大簡(jiǎn)化了異步操作,包括網(wǎng)絡(luò)請(qǐng)求相關(guān)代碼。
RAC的一個(gè)主要優(yōu)點(diǎn)是它提供了一個(gè)單一的、統(tǒng)一的方法來處理異步行為,包括delegate、blocks、target-action機(jī)制、notifications和KVO。
簡(jiǎn)單例子:
// 當(dāng) self.username 改變, 打印最新的值
//
// RACObserve(self, username) 創(chuàng)建了一個(gè)新的信號(hào)量用來發(fā)送信號(hào)
// selg.username每當(dāng)改變時(shí)就會(huì)有一個(gè)新值。
// 將在信號(hào)發(fā)送一個(gè)新值時(shí)執(zhí)行subscribeNext相關(guān)block方法
[RACObserve(self, username) subscribeNext:^(NSString *newName) {
NSLog(@"%@", newName);
}];
但是和KVO通知不同的是,Signals可以組合并共同操作:
//只打印以“j”開頭的名字
//
//-filter返回一個(gè)新的RACSignal,當(dāng)它的內(nèi)容返回YES時(shí)。
[[RACObserve(self, username)
filter:^(NSString *newName) {
return [newName hasPrefix:@"j"];
}]
subscribeNext:^(NSString *newName) {
NSLog(@"%@", newName);
}];
Signals也可以用來獲取狀態(tài)。RAC不需要觀察屬性并根據(jù)新值設(shè)置其他屬性,而是可以用信號(hào)和操作來表示屬性:
//創(chuàng)建一個(gè)單向綁定用判斷self.createEnabled和self.password的值是否相等,若相等則self.createEnabled等于true
//RAC()是一個(gè)很好的綁定宏
//
//+combineLatest接收一個(gè)數(shù)組,用來表示當(dāng)任何信號(hào)發(fā)送變化時(shí),獲取每個(gè)信號(hào)的最新值并返回一個(gè)信號(hào)量并將最新值返回。
RAC(self, createEnabled) = [RACSignal
combineLatest:@[ RACObserve(self, password), RACObserve(self, passwordConfirmation) ]
reduce:^(NSString *password, NSString *passwordConfirm) {
return @([passwordConfirm isEqualToString:password]);
}];
不止KVO,Signals可以隨著時(shí)間改變建立在任何stream值上。例如,它可以用來表示按下按鈕:
// 當(dāng)按下按鈕時(shí)記錄信息
//
// RACCommand 創(chuàng)建一個(gè)信號(hào)來表示UI操作。例如,每個(gè)信號(hào)可以表示按下按鈕及與之相關(guān)的操作。
//
// -rac_command 是對(duì)NSButton的補(bǔ)充。每當(dāng)按鈕被按下時(shí)他就會(huì)發(fā)送命令給自己。
self.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id _) {
NSLog(@"button was pressed!");
return [RACSignal empty];
}];
或者異步網(wǎng)絡(luò)操作:
//點(diǎn)擊登錄按鈕是進(jìn)行網(wǎng)絡(luò)請(qǐng)求
//
//當(dāng)執(zhí)行登錄操作時(shí),這個(gè)block將被運(yùn)行,程序執(zhí)行登錄操作
self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^(id sender) {
// 假設(shè)-login方法返回一個(gè)信號(hào),當(dāng)網(wǎng)絡(luò)請(qǐng)求完成時(shí)返回一個(gè)值。
return [client logIn];
}];
//-executionSignals返回一個(gè)信號(hào),該信號(hào)每當(dāng)執(zhí)行一次都有一個(gè)包含上述返回的信號(hào)
[self.loginCommand.executionSignals subscribeNext:^(RACSignal *loginSignal) {
// 當(dāng)?shù)卿洺晒r(shí)打印信息
[loginSignal subscribeCompleted:^{
NSLog(@"Logged in successfully!");
}];
}];
//按下登錄按鈕執(zhí)行登錄操作
self.loginButton.rac_command = self.loginCommand;
Signals還可以用作定時(shí)器,其他UI事件,或者任何隨著時(shí)間變化的東西。
在異步操作中可以通過連接和轉(zhuǎn)換這些信號(hào)來構(gòu)建更為復(fù)雜的操作。在操作完成后可以輕松的觸發(fā):
//執(zhí)行2個(gè)網(wǎng)絡(luò)請(qǐng)求,完成后進(jìn)行打印操作
//
// +merger:接收一個(gè)信號(hào)數(shù)組并返回一個(gè)新的RACSignal,該RACSignal傳遞所有信號(hào)的值,并在所有信號(hào)完成后進(jìn)行完成。
//
//-subscribeCompleted:將在信號(hào)完成時(shí)執(zhí)行
[[RACSignal
merge:@[ [client fetchUserRepos], [client fetchOrgRepos] ]]
subscribeCompleted:^{
NSLog(@"They're both done!");
}];
Signals可以鏈接起來以順序執(zhí)行的方式進(jìn)行異步操作,而不是用block嵌套的方式回調(diào)。這類似于futures和promise的用法:
//記錄用戶,然后加載所有緩存的消息,然后從服務(wù)器獲取剩余的消息。完成所有操作后,將一條消息記錄到控制臺(tái)。
//
//假設(shè)-logInUser方法返回一個(gè)在登錄完成后的信號(hào)。
//
//-flattenMap:每當(dāng)有一個(gè)信號(hào)發(fā)送值是,將執(zhí)行該block代碼,并返回一個(gè)將所有返回信號(hào)合并成單一信號(hào)的RACSignal。
[[[[client
logInUser]
flattenMap:^(User *user) {
// 返回一個(gè)用戶加載緩存消息的信號(hào)。
return [client loadCachedMessagesForUser:user];
}]
flattenMap:^(NSArray *messages) {
// 返回一個(gè)獲取任何剩余消息的信號(hào)。
return [client fetchMessagesAfterMessage:messages.lastObject];
}]
subscribeNext:^(NSArray *newMessages) {
NSLog(@"New messages: %@", newMessages);
} completed:^{
NSLog(@"Fetched all messages.");
}];
RAC也可以很容易綁定異步操作的結(jié)果:
//創(chuàng)建一個(gè)self.imageView.image一單被下載就會(huì)被設(shè)置成為用戶頭像的單向綁定。
//
//假設(shè)-fetchUserWithUsername:方法返回一個(gè)發(fā)送給用戶的信號(hào)。
//
//-deliverOn:創(chuàng)建一個(gè)在其他隊(duì)列上工作的新信號(hào)。在本例中,它用于將工作轉(zhuǎn)移到后臺(tái)隊(duì)列,然后返回到主線程。
//
//-map:對(duì)每個(gè)調(diào)用block的用戶,返回一個(gè)新的RACSignal,發(fā)送block返回的值。
RAC(self.imageView, image) = [[[[client
fetchUserWithUsername:@"joshaber"]
deliverOn:[RACScheduler scheduler]]
map:^(User *user) {
// 下載角色(這是在后臺(tái)隊(duì)列中完成的)。
return [[NSImage alloc] initWithContentsOfURL:user.avatarURL];
}]
// 賦值將在主線程完成
deliverOn:RACScheduler.mainThreadScheduler];
何時(shí)使用ReactiveObjc
初次接觸,ReactiveObjc非常的抽象,很難想象如何將其運(yùn)用于具體的問題。
以下是RAC擅長(zhǎng)解決問題的一些用例。
處理異步或是事件驅(qū)動(dòng)的數(shù)據(jù)源
大部分Cocoa編程都專注于響應(yīng)用戶事件或應(yīng)用程序狀態(tài)的改變。處理這類事件的代碼可能會(huì)變得非常復(fù)雜,就像意大利面一樣,要用很多回調(diào)和狀態(tài)變量來處理順序問題。
表面上看起來不同的模式,如UI回調(diào)、網(wǎng)絡(luò)響應(yīng)和KVO通知,實(shí)際上有很多相同之處。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];
}
鏈接回調(diào)操作
依賴關(guān)系最常見于網(wǎng)絡(luò)請(qǐng)求中,即前一個(gè)請(qǐng)求完成才能進(jìn)行下一個(gè)請(qǐng)求,以此類推:
[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];
}];
ReactiveObjc讓這中模式變得特別容易:
[[[[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.");
}];
并發(fā)操作
并發(fā)處理各自的數(shù)據(jù)集合,然后將他們組合在一起生產(chǎn)最終結(jié)果,這在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通過簡(jiǎn)單的組合來優(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");
}];
簡(jiǎn)化集合轉(zhuǎn)換
像map,filter, fold/reduce這樣的高階函數(shù)在Foundation中嚴(yán)重缺失,導(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允許以統(tǒng)一且聲明的方式操作任何Cocoa集合:
RACSequence *results = [[strings.rac_sequence
filter:^ BOOL (NSString *str) {
return str.length >= 2;
}]
map:^(NSString *str) {
return [str stringByAppendingString:@"foobar"];
}];
注:以上內(nèi)容翻譯于github官方文檔,如有錯(cuò)誤,歡迎指正。