初識(shí)ReactiveCocoa

原文 : 與佳期的個(gè)人博客(gonghonglou.com)

ReactiveCocoa 是一個(gè)Objective-C 框架,受 Functional Reactive Programming的啟發(fā)。它提供了一系列用來組合和轉(zhuǎn)換值流的API。

如果你早已熟悉了函數(shù)響應(yīng)式編程或者知道ReactiveCocoa的基本前提,看看Documentation這個(gè)文件夾里的framework overview等文件來了解它是怎樣在實(shí)踐中工作的。

介紹

ReactiveCocoa受functional reactive programming的啟發(fā)。在那些能被替換和修改的地方,RAC提供信號(hào)(由RACSignal代表)來捕獲當(dāng)前和將來的值而不是使用可變的變量。

一個(gè)文本框能夠根據(jù)它的改變被綁定到最后一次的值,而不是使用額外的代碼每秒去監(jiān)控時(shí)鐘和更新文本框。這點(diǎn)跟KVO很像,不過使用了block,而不是-observeValueForKeyPath:ofObject:change:context:

信號(hào)也可以進(jìn)行異步操作,就像futures and promises。這極大的簡化了異步軟件中網(wǎng)絡(luò)連接的代碼。

RAC的重大優(yōu)勢(shì)之一就是它提供信號(hào)(signal)這種方式來統(tǒng)一的處理所有異步的行為,包括代理方法、block 回調(diào)、target-action 機(jī)制、通知和KVO。

這里是簡單的例子:

// 當(dāng)self.username改變時(shí),打印新的名字到控制臺(tái)
//
// RACObserve(self, username)創(chuàng)建一個(gè)新的RACSignal,當(dāng)前self.username的值發(fā)生改變時(shí),發(fā)送新值給newName
// -subscribeNext: 當(dāng)信號(hào)發(fā)送值時(shí)將觸發(fā)block
[RACObserve(self, username) subscribeNext:^(NSString *newName) {
    NSLog(@"%@", newName);
}];

與KVO 通知不同的是信號(hào)能夠進(jìn)行統(tǒng)一的鏈?zhǔn)讲僮鳎?/p>

// 只有當(dāng)名字的開頭為"j"時(shí)才打印
//
// -filter 只有當(dāng)block返回YES時(shí)才會(huì)創(chuàng)建一個(gè)新的RACSignal發(fā)送一個(gè)新值
[[RACObserve(self, username)
    filter:^(NSString *newName) {
        return [newName hasPrefix:@"j"];
    }]
    subscribeNext:^(NSString *newName) {
        NSLog(@"%@", newName);
    }];

信號(hào)也能被用來派生狀態(tài)。在響應(yīng)新值中RAC代替觀察屬性和設(shè)置其他的屬性,能夠在信號(hào)和運(yùn)行周期內(nèi)傳達(dá)屬性:

// 當(dāng)self.password 和 self.passwordConfirmation相同時(shí)創(chuàng)建一個(gè)單向的binding使得self.createEnabled為true
//
// RAC() 是一個(gè)宏指令使得binding看起來nicer
// 
// +combineLatest:reduce: 建一個(gè)信號(hào)數(shù)組
// 當(dāng)任一個(gè)信號(hào)的最后一個(gè)值發(fā)生改變時(shí)觸發(fā)這個(gè)block,返回一個(gè)新的RACSignal,將block返回的值作為values發(fā)送出去
RAC(self, createEnabled) = [RACSignal 
    combineLatest:@[ RACObserve(self, password), RACObserve(self, passwordConfirmation) ] 
    reduce:^(NSString *password, NSString *passwordConfirm) {
        return @([passwordConfirm isEqualToString:password]);
    }];

信號(hào)不僅是在KVO上,還能在建立在隨著時(shí)間而改變的值流上。例如,它們可以代表按鈕點(diǎn)擊:

// 當(dāng)按鈕被點(diǎn)擊時(shí)打印信息
//
// RACCommand創(chuàng)建信號(hào)去表示UI行為。例如,每一個(gè)信號(hào)可以表示一個(gè)按鈕的點(diǎn)擊、與它相關(guān)聯(lián)的附加工作
//
// -rac_command是封裝的NSButton方法. 當(dāng)按鈕被點(diǎn)擊時(shí)將發(fā)送到該命令
self.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id _) {
    NSLog(@"button was pressed!");
    return [RACSignal empty];
}];

或者是異步網(wǎng)絡(luò)操作:

// 連接"Log in"按鈕給網(wǎng)絡(luò)登錄
//
// 當(dāng)?shù)卿浢顖?zhí)行時(shí)運(yùn)行block,開始登錄進(jìn)度
self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^(id sender) {
    // 假設(shè)當(dāng)網(wǎng)絡(luò)請(qǐng)求完成時(shí) -logIn 方法返回一個(gè)信號(hào)發(fā)送一個(gè)value
    return [client logIn];
}];

// -executionSignals 每次執(zhí)行該命令時(shí),這個(gè)方法返回一個(gè)信號(hào),包括以前的block返回的信號(hào)
[self.loginCommand.executionSignals subscribeNext:^(RACSignal *loginSignal) {
    // 成功登錄時(shí)打印信息
    [loginSignal subscribeCompleted:^{
        NSLog(@"Logged in successfully!");
    }];
}];

// 按鈕被點(diǎn)擊時(shí)執(zhí)行登錄命令
self.loginButton.rac_command = self.loginCommand;

信號(hào)也可以代表定時(shí)器,其他的UI事件,或者別的什么隨時(shí)間而改變的事件。

在異步操作方面,通過鏈接和轉(zhuǎn)換信號(hào)可以建立更復(fù)雜的行為。在一組完整的操作之后更簡單的來執(zhí)行工作:

// 執(zhí)行2個(gè)網(wǎng)絡(luò)操作,當(dāng)它們都完成時(shí)打印信息到控制臺(tái)
//
// +merge: 當(dāng)數(shù)組里的所有信號(hào)完成時(shí),返回一個(gè)新的RACSignal
//
// -subscribeCompleted: 當(dāng)信號(hào)完成時(shí)將執(zhí)行這個(gè)block
[[RACSignal 
    merge:@[ [client fetchUserRepos], [client fetchOrgRepos] ]] 
    subscribeCompleted:^{
        NSLog(@"They're both done!");
    }];

信號(hào)可以被鏈接到順序執(zhí)行異步操作,而不是使用一堆block回調(diào)。通常這樣簡單的來使用futures and promises

// 用戶登錄,下載緩存信息,獲取服務(wù)器信息。都完成后將信息打印到控制臺(tái)
//
// 假設(shè)登錄之后 -logInUser 方法返回一個(gè)信號(hào)
//
// -flattenMap: 當(dāng)信號(hào)發(fā)送一個(gè)value時(shí)觸發(fā)這個(gè)block
// 并且返回一個(gè)新的RACSignal來整合從block返回的所有的信號(hào)到一個(gè)單一信號(hào)中
[[[[client 
    logInUser] 
    flattenMap:^(User *user) {
        // 下載緩存信息,給用戶返回一個(gè)信號(hào)
        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甚至可以簡單的建立在一個(gè)異步操作的結(jié)果上:

// 創(chuàng)建一個(gè)單向的binding,讓 self.imageView.image 來放置下載下來的user的頭像
//
// 假設(shè) -fetchUserWithUsername: 方法返回一個(gè)信號(hào)發(fā)送給user
//
// -deliverOn: 創(chuàng)建新的信號(hào)在其他的隊(duì)列中進(jìn)行他們的工作
// 在這個(gè)例子中,此方法被用來將工作轉(zhuǎn)移到后臺(tái)隊(duì)列和回到主線程
//
// -map: 每個(gè)user調(diào)用這個(gè)block,獲取并且返回一個(gè)新的RACSignal,并且將從block返回的值發(fā)送出去
RAC(self.imageView, image) = [[[[client 
    fetchUserWithUsername:@"joshaber"]
    deliverOn:[RACScheduler scheduler]]
    map:^(User *user) {
        // 下載頭像 (在后臺(tái)隊(duì)列中進(jìn)行).
        return [[NSImage alloc] initWithContentsOfURL:user.avatarURL];
    }]
    // 此時(shí)這個(gè)任務(wù)將在主線程中執(zhí)行
    deliverOn:RACScheduler.mainThreadScheduler];

這是一些使用RAC的示范操作,但是它并不能說明RAC為什么如此強(qiáng)大。
更多示例代碼參見C-41GroceryList,這些是使用ReactiveCocoa編寫的iOS APP。在這個(gè)文件夾Documentation中可以查到更多的關(guān)于RAC的信息。

使用ReactiveCocoa

乍一看ReactiveCocoa是非常抽象的,很難理解該怎樣將它應(yīng)用到具體的問題上。

這有一些示例來展示RAC的優(yōu)勢(shì)

處理異步或事件驅(qū)動(dòng)的數(shù)據(jù)源

許多Cocoa編程的重點(diǎn)是對(duì)用戶事件的反應(yīng)或應(yīng)用狀態(tài)的變化。處理這些事件的代碼很快變得非常復(fù)雜的就像意大利面一樣,伴隨著許多回調(diào)函數(shù)和狀態(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];
}

鏈接依賴操作

依賴在網(wǎng)絡(luò)請(qǐng)求中是常見的,在下一個(gè)請(qǐng)求建立之前,需要完成當(dāng)前對(duì)服務(wù)器的請(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];
}];

在ReactiveCocoa中可以這樣簡單的實(shí)現(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.");
    }];

并行獨(dú)立工作

與獨(dú)立的數(shù)據(jù)集合并行工作,然后將它們合并成一個(gè)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];

上面的代碼可以用簡單的合成信號(hào)來清理和優(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的基本用法,僅限于使用,所以墻裂建議仔細(xì)學(xué)習(xí)下節(jié)的RAC學(xué)習(xí)資料,了解RAC原理及高階用法。

  • 小白出手,請(qǐng)多指教。如言有誤,還望斧正!

  • 轉(zhuǎn)載請(qǐng)保留原文地址http://gonghonglou.com/2016/03/17/meet-ReactiveCocoa

RAC學(xué)習(xí)資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容