ReactiveObjC學(xué)習(xí)筆記

這篇文章的內(nèi)容絕大部分翻譯自github上的ReactiveObjC

ReactiveObjC

注意 : 這是ReactiveCocoa Objective-C的介紹,ReactiveCocoa的OC版本現(xiàn)在叫做ReactiveObjC了, ReactiveCocoa的升級版本是用Swift語言寫的,想了解它的升級版本,請看ReactiveCocoa 或者 ReactiveSwift例嘱。

ReactiveObjC(ReactiveCocoa 或 RAC) 是一個基于函數(shù)響應(yīng)式編程思想的Objective-C框架, 它提供了各種APIs,這些 APIs 可用于組合,轉(zhuǎn)換數(shù)據(jù)流。

簡介

ReactiveObjC(RAC)是一個函數(shù)響應(yīng)式編程框架颖变。RAC用信號(類名為RACSignal)來代替和處理各種變量的變化和傳遞掌实。

通過信號signals的傳輸陪蜻,重新組合和響應(yīng),軟件代碼的編寫邏輯思路將變得更清晰緊湊潮峦,有條理,而不再需要對變量的變化不斷的觀察更新勇婴。

例如忱嘹,UITextField的文本內(nèi)容(text)剛起了變化,就要立即作出響應(yīng)(UITextField還沒失去焦點)更新動作耕渴,我們是不會看著手表來時刻觀察更新textField,類似于采取KVO措施一樣重寫的-observeValueForKeyPath:ofObject:change:context: 拘悦,而是通過信號Signals的block實現(xiàn)這一動作。

信號Signals還能夠代替實現(xiàn)異步操作橱脸,或者是并發(fā)處理問題础米。這大大的簡化了異步操作(如網(wǎng)絡(luò))的代碼分苇。

RAC的主要好處是它提供了一個信號Signal,來統(tǒng)一處理Cocoa的各種行為屁桑,包括delegate-methods,block回調(diào)医寿,target-action機(jī)制,通知和KVO等等蘑斧。

這里是個簡單的例子:

//當(dāng)self.username 改變時靖秩,在控制臺打印出新的username
//宏定義RACObserve(self, username)會創(chuàng)建一個新的RACSignal信號,只要self.username的值有新變化竖瘾,信號就會發(fā)送傳遞self.username新的值
//當(dāng)信號signal傳送一個數(shù)據(jù)時沟突,-subscribeNext:將執(zhí)行block里的語句
[RACObserve(self, username) subscribeNext:^(NSString *newName) {
    NSLog(@"%@", newName);
}];

但與KVO通知模式不同,信號signals是可以被串聯(lián)起來進(jìn)行操作:

// 只打印以'j'開頭的username.
//當(dāng)-filter的block返回YES時捕传,-filter將會返回一個新的RACSigal信號惠拭,這個信號只傳送username新的值
[[RACObserve(self, username)
    filter:^(NSString *newName) {
        return [newName hasPrefix:@"j"];
    }]
    subscribeNext:^(NSString *newName) {
        NSLog(@"%@", newName);
    }];

Signals 也能夠用于派生出新的東西,RAC通過信號傳送和信號操作庸论,令屬性重新賦值成為可能(不是通過傳統(tǒng)的監(jiān)聽屬性和為屬性設(shè)置其它值來響應(yīng)屬性的變化):

// 創(chuàng)建一個單向綁定职辅,當(dāng)self.password與self.passwordConfirmation相等時,self.createEnabled將被賦值為true
// RAC()是一個讓綁定看起來更好的宏
// +combineLatest:reduce:接收一個存有信號的數(shù)組葡公,當(dāng)數(shù)組中任一信號更新值時罐农,block就會被執(zhí)行并會以block的返回值作為信號的傳遞因子創(chuàng)建新的信號RACSignal并返回此信號
RAC(self, createEnabled) = [RACSignal
    combineLatest:@[ RACObserve(self, password), RACObserve(self, passwordConfirmation) ]
    reduce:^(NSString *password, NSString *passwordConfirm) {
        return @([passwordConfirm isEqualToString:password]);
    }];

不僅是KVO可以,信號Signals也可以基于任何數(shù)據(jù)流而被創(chuàng)建催什。比方說涵亏,信號可以表示button按壓事件

//只要button被按壓了,就打印信息
//
//RACCommand類會創(chuàng)建信號來表示UI控件的動作事件蒲凶,比如說气筋,信號可以表示button的一次按壓事件或者一些與button相關(guān)的其它事件,當(dāng)事件發(fā)生時,signalBlock將會被執(zhí)行并返回
//
//-rac_command 除了用在Button外旋圆,還可以用在其它UI控件宠默。當(dāng)button被按壓時,button將向自己發(fā)送這個指令(command)

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í)行開始登錄操作時,這個block將被運行搀矫,這個block返回一個RACSignal
self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^(id sender) {
    //假設(shè):-logIn 方法r 返回值是一個信號(這個信號能夠發(fā)送一個數(shù)據(jù)當(dāng)網(wǎng)絡(luò)請求結(jié)束時)
    return [client logIn];
}];

//-executionSignals會返回一個信號,這個信號取自上面Signalblock的每次運行返回的值
[self.loginCommand.executionSignals subscribeNext:^(RACSignal *loginSignal) {
    //登錄成功后就打印
    [loginSignal subscribeCompleted:^{
        NSLog(@"Logged in successfully!");
    }];
}];

//button被按壓刻肄,將執(zhí)行登錄事件指令
self.loginButton.rac_command = self.loginCommand;

Signals還可以表示時鐘或其它UI事件瓤球,或者任何隨時間發(fā)生改變的事件。

通過節(jié)節(jié)緊扣的鏈?zhǔn)骄幊毯蛡魉瓦@些信號敏弃,讓異步操作處理更多復(fù)雜的事件成為可能卦羡。在一系列動作(數(shù)據(jù)請求,驗證,格式化等)完成后绿饵,緊接的動作會很容易地被觸發(fā)欠肾。

//執(zhí)行2個網(wǎng)絡(luò)任務(wù)并在控制臺打印信息,當(dāng)2個網(wǎng)絡(luò)任務(wù)都完成時拟赊。
//+merge類方法持有一個信號數(shù)組并返回一個新的RACSignal信號刺桃。當(dāng)數(shù)組中所有信號都完成時,這個新信號會傳遞數(shù)組中所有信號包裹的數(shù)據(jù)要门。
//當(dāng)新信號結(jié)束傳遞時虏肾,-subscribeCompleted將執(zhí)行block
[[RACSignal
    merge:@[ [client fetchUserRepos], [client fetchOrgRepos] ]]
    subscribeCompleted:^{
        NSLog(@"They're both done!");
    }];

Signals 可以不通過嵌套用作回調(diào)的block來順序地執(zhí)行異步操作,這與同步處理有相似之處

//用戶進(jìn)行登錄欢搜,加載緩存信息封豪,然后從服務(wù)器拉取余下的信息。等這些動作完成后炒瘟,在控制臺打印信息
//假設(shè)-logInUser 方法在登錄完成后返回一個signal
//當(dāng)信號發(fā)送一個數(shù)據(jù)時吹埠,-flattenMap:的block將被執(zhí)行,并在block執(zhí)行后-flattenMap:返回一個新的信號RACSignal,這個信號將block返回的全部signal都并入進(jìn)來
[[[[client
    logInUser]
    flattenMap:^(User *user) {
        //返回一個信號疮装,這個信號包裹用戶加載緩存的信息
        return [client loadCachedMessagesForUser:user];
    }]
    flattenMap:^(NSArray *messages) {
        //返回一個信號缘琅,這個信號包裹從網(wǎng)絡(luò)上拉取剩余的信號
        return [client fetchMessagesAfterMessage:messages.lastObject];
    }]
    subscribeNext:^(NSArray *newMessages) {
        NSLog(@"New messages: %@", newMessages);
    } completed:^{
        NSLog(@"Fetched all messages.");
    }];

甚至,RAC可以很容易地綁定異步操作的結(jié)果:

//創(chuàng)建一個單向綁定廓推,目的是:只要圖片下載完成就把它設(shè)成用戶的頭像圖片
//假設(shè):--fetchUserWithUsername:方法返回一個信號刷袍,這個信號用于傳送user對象
//-deliverOn:會創(chuàng)建新的信號,這些信號將工作在后臺隊列樊展。在這個例子中呻纹,deliverOn方法將任務(wù)搬到后臺隊列去工作,然后返回到主線程
//-map:傳參user給block并調(diào)用它专缠,-map:執(zhí)行結(jié)后會返回一個新的RACSignal,這個信號傳送的數(shù)據(jù)是來自block的返回值

RAC(self.imageView, image) = [[[[client
    fetchUserWithUsername:@"joshaber"]
    deliverOn:[RACScheduler scheduler]]
    map:^(User *user) {
    //下載圖像數(shù)據(jù)(這個任務(wù)在后臺隊列執(zhí)行)
        return [[NSImage alloc] initWithContentsOfURL:user.avatarURL];
    }]
    // 此時雷酪,任務(wù)運行在主線程
    deliverOn:RACScheduler.mainThreadScheduler];

想看看真正使用RAC創(chuàng)建的項目?請check out C-41GroceryList, 它們都是用ReactiveObjC鏈?zhǔn)巾憫?yīng)編程框架寫的iOS app涝婉。

當(dāng)你要使用ReactiveObjc時

粗略地回頭看看上面的說明哥力,ReactiveObjC 真的是挺抽象的,而且它讓人感覺會很困難,如果把它應(yīng)用在具體的問題上墩弯。

處理異步任務(wù)或事件驅(qū)動數(shù)據(jù)更新

在Cocoa框架下編程吩跋,大多數(shù)都會關(guān)注如何響應(yīng)用戶交互或者改變app的狀態(tài)(更新頁面數(shù)據(jù)或其它),這讓處理這些事件的代碼很快變得像意大利一樣渔工,亂成一團(tuán)并且很復(fù)雜锌钮,因為這些代碼會用到大量的回調(diào)和狀態(tài)變量去handle問題。

RAC的編程模式在表面上看涨缚,跟UI callbacks,網(wǎng)絡(luò)請求響應(yīng)和KVO通知很不一樣轧粟,實際上它們是極大地相同的。RACSignal信號只不過把它們不同的APIs統(tǒng)一了起來脓魏,目的是讓他們變得可組合且能夠以相同的方式來操作兰吟。

例如,下面的代碼:

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];
}

串聯(lián)相關(guān)的操作(下一任務(wù)需要上一任務(wù)的執(zhí)行結(jié)果)

在網(wǎng)絡(luò)請求中茂翔,相互關(guān)聯(lián)的事件是非常普遍的混蔼,下一請求需要在上一請求完成后才能被發(fā)起,像如下:

[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.");
    }];

平行不相關(guān)的任務(wù)(并發(fā)任務(wù)珊燎,各自執(zhí)行各自的)

把幾個并發(fā)任務(wù)的執(zhí)行結(jié)果(數(shù)據(jù))歸并成最終的結(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];

上面的代碼可以通過簡單地組合signal信號來清減和優(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");
    }];

簡化集合類的轉(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集合類以統(tǒng)一和見文知義的方式來操作處理:

RACSequence *results = [[strings.rac_sequence
    filter:^ BOOL (NSString *str) {
        return str.length >= 2;
    }]
    map:^(NSString *str) {
        return [str stringByAppendingString:@"foobar"];
    }];

系統(tǒng)配置

ReactiveObjC 支持 OS X 10.8+ 和 iOS 8.0+

導(dǎo)入ReactiveObjC

這樣來將RAC添加到你的App:

一:使用CocoaPods來管理RAC

前提:已經(jīng)安裝了CocoaPods

  1. Podfile寫上pod 'ReactiveObjC', '~>3.0.0'
  2. 更新pod依賴:進(jìn)入到工程目錄下谋国,在終端輸入pod update --no-repo-update 然后回車
  3. 導(dǎo)入#import "ReactiveObjC.h"
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末槽地,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子芦瘾,更是在濱河造成了極大的恐慌捌蚊,老刑警劉巖,帶你破解...
    沈念sama閱讀 223,002評論 6 519
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件近弟,死亡現(xiàn)場離奇詭異缅糟,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)祷愉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,357評論 3 400
  • 文/潘曉璐 我一進(jìn)店門窗宦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人谣辞,你說我怎么就攤上這事迫摔。” “怎么了泥从?”我有些...
    開封第一講書人閱讀 169,787評論 0 365
  • 文/不壞的土叔 我叫張陵句占,是天一觀的道長。 經(jīng)常有香客問我躯嫉,道長纱烘,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,237評論 1 300
  • 正文 為了忘掉前任祈餐,我火速辦了婚禮擂啥,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘帆阳。我一直安慰自己哺壶,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 69,237評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著山宾,像睡著了一般至扰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上资锰,一...
    開封第一講書人閱讀 52,821評論 1 314
  • 那天敢课,我揣著相機(jī)與錄音,去河邊找鬼绷杜。 笑死直秆,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的鞭盟。 我是一名探鬼主播圾结,決...
    沈念sama閱讀 41,236評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼齿诉!你這毒婦竟也來了疫稿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,196評論 0 277
  • 序言:老撾萬榮一對情侶失蹤鹃两,失蹤者是張志新(化名)和其女友劉穎遗座,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體俊扳,經(jīng)...
    沈念sama閱讀 46,716評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡途蒋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,794評論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了馋记。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片号坡。...
    茶點故事閱讀 40,928評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖梯醒,靈堂內(nèi)的尸體忽然破棺而出宽堆,到底是詐尸還是另有隱情,我是刑警寧澤茸习,帶...
    沈念sama閱讀 36,583評論 5 351
  • 正文 年R本政府宣布畜隶,位于F島的核電站,受9級特大地震影響号胚,放射性物質(zhì)發(fā)生泄漏籽慢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,264評論 3 336
  • 文/蒙蒙 一猫胁、第九天 我趴在偏房一處隱蔽的房頂上張望箱亿。 院中可真熱鬧,春花似錦弃秆、人聲如沸届惋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,755評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽脑豹。三九已至氢卡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間晨缴,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,869評論 1 274
  • 我被黑心中介騙來泰國打工峡捡, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留击碗,地道東北人。 一個月前我還...
    沈念sama閱讀 49,378評論 3 379
  • 正文 我出身青樓们拙,卻偏偏與公主長得像稍途,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子砚婆,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,937評論 2 361

推薦閱讀更多精彩內(nèi)容