iOS ReactiveObjC使用

介紹

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)異步操作,就像futurespromises一樣。這大大簡(jiǎn)化了異步操作,包括網(wǎng)絡(luò)請(qǐng)求相關(guān)代碼。

RAC的一個(gè)主要優(yōu)點(diǎn)是它提供了一個(gè)單一的、統(tǒng)一的方法來處理異步行為,包括delegate、blocks、target-action機(jī)制、notificationsKVO。

簡(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]);
    }];

不止KVOSignals可以隨著時(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)。這類似于futurespromise的用法:

//記錄用戶,然后加載所有緩存的消息,然后從服務(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ò)誤,歡迎指正。

?著作權(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),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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