初識ReactiveCocoa

原文 : 與佳期的個人博客(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-41GroceryList,這些是使用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í)資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末泡态,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子迂卢,更是在濱河造成了極大的恐慌某弦,老刑警劉巖,帶你破解...
    沈念sama閱讀 223,126評論 6 520
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件而克,死亡現(xiàn)場離奇詭異靶壮,居然都是意外死亡,警方通過查閱死者的電腦和手機员萍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,421評論 3 400
  • 文/潘曉璐 我一進店門腾降,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人碎绎,你說我怎么就攤上這事螃壤。” “怎么了筋帖?”我有些...
    開封第一講書人閱讀 169,941評論 0 366
  • 文/不壞的土叔 我叫張陵奸晴,是天一觀的道長。 經(jīng)常有香客問我日麸,道長寄啼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,294評論 1 300
  • 正文 為了忘掉前任代箭,我火速辦了婚禮墩划,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘嗡综。我一直安慰自己乙帮,他們只是感情好,可當我...
    茶點故事閱讀 69,295評論 6 398
  • 文/花漫 我一把揭開白布蛤高。 她就那樣靜靜地躺著蚣旱,像睡著了一般碑幅。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上塞绿,一...
    開封第一講書人閱讀 52,874評論 1 314
  • 那天沟涨,我揣著相機與錄音,去河邊找鬼异吻。 笑死裹赴,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的诀浪。 我是一名探鬼主播棋返,決...
    沈念sama閱讀 41,285評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼雷猪!你這毒婦竟也來了睛竣?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,249評論 0 277
  • 序言:老撾萬榮一對情侶失蹤求摇,失蹤者是張志新(化名)和其女友劉穎扶平,沒想到半個月后茂附,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體钞支,經(jīng)...
    沈念sama閱讀 46,760評論 1 321
  • 正文 獨居荒郊野嶺守林人離奇死亡郊楣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,840評論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了摔刁。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挥转。...
    茶點故事閱讀 40,973評論 1 354
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖共屈,靈堂內(nèi)的尸體忽然破棺而出绑谣,到底是詐尸還是另有隱情,我是刑警寧澤拗引,帶...
    沈念sama閱讀 36,631評論 5 351
  • 正文 年R本政府宣布域仇,位于F島的核電站,受9級特大地震影響寺擂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜泼掠,卻給世界環(huán)境...
    茶點故事閱讀 42,315評論 3 336
  • 文/蒙蒙 一怔软、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧择镇,春花似錦挡逼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,797評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嘱能。三九已至,卻和暖如春虱疏,著一層夾襖步出監(jiān)牢的瞬間惹骂,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,926評論 1 275
  • 我被黑心中介騙來泰國打工做瞪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留对粪,地道東北人。 一個月前我還...
    沈念sama閱讀 49,431評論 3 379
  • 正文 我出身青樓装蓬,卻偏偏與公主長得像著拭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子牍帚,可洞房花燭夜當晚...
    茶點故事閱讀 45,982評論 2 361

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