ReactiveCocoa教程 - 入門:1/2

注意: 此文只是自己翻譯學(xué)習(xí)雷滋,如有不對(duì)地方還望指出。此文結(jié)合如下倆篇文章翻譯臼勉,一則練習(xí)自己的翻譯能力邻吭,二則真正理解該文章。
個(gè)人感悟:自己動(dòng)手寫一遍宴霸,與瀏覽一遍囱晴,絕對(duì)是不一樣的
原文鏈接
已有翻譯文章

作為一名iOS開發(fā)者, 你寫的每一行代碼幾乎都是對(duì)某些事件的反饋:點(diǎn)擊button、接收到網(wǎng)絡(luò)信息瓢谢、 一個(gè)屬性的改變(通過(guò)KVO監(jiān)測(cè)) 或者 通過(guò)CoreLocation監(jiān)聽用戶所在位置的變化 以上等等都是很好的例子畸写。然而,這些事件都有不同的編碼方式氓扛,如: action枯芬、delegate、KVO采郎、回調(diào)等千所。ReactiveCocoa為事件定義了標(biāo)準(zhǔn)的接口,從而可以使用一些基本工具來(lái)更容易的連接尉剩、過(guò)濾和組合真慢。

聽起來(lái)很困惑毅臊?奇妙理茎?...令人興奮?接著往下看:
ReactiveCocoa結(jié)合了幾個(gè)編程風(fēng)格:

  • Functional Programming : 函數(shù)式編程, 使用了高階函數(shù)管嬉,即函數(shù)采用了多種函數(shù)為它們的參數(shù)
  • Reactive Programming : 響應(yīng)式編程皂林,側(cè)重?cái)?shù)據(jù)流和變化傳遞

出于這個(gè)原因,你可能會(huì)聽到ReactiveCocoa被描述為一個(gè) 函數(shù)響應(yīng)式編程 (FRP)框架蚯撩。

放心础倍,這個(gè)技術(shù)會(huì)在本教程中Get到!編程范式是一個(gè)不錯(cuò)的討論主題胎挎,但這個(gè)ReactiveCocoa教程是一個(gè)實(shí)際的例子沟启,而不是學(xué)術(shù)理論。

The Reactive Playground

在這個(gè)Reactive教程中犹菇,你將在一個(gè)非常簡(jiǎn)單的事例應(yīng)用中引入響應(yīng)式編程德迹。下載示例項(xiàng)目,然后編譯運(yùn)行以保證已經(jīng)擁有基本設(shè)置揭芍。(根據(jù)示例項(xiàng)目我也模仿編寫一個(gè)Demo)

ReactivePlayground是一個(gè)非常簡(jiǎn)單的應(yīng)用程序胳搞,提供了一個(gè)登錄界面。當(dāng)用戶輸入正確的用戶名和密碼時(shí),一只可愛(ài)的小貓就會(huì)映入眼簾肌毅。

登錄頁(yè)面

登錄成功后的頁(yè)面

現(xiàn)在來(lái)花時(shí)間看看這個(gè)簡(jiǎn)單Demo的代碼筷转。很簡(jiǎn)單,浪費(fèi)多長(zhǎng)時(shí)間悬而。

打開 ViewController.m呜舒,全局的翻閱一下。你能否快速定位到 SignIn 的enabled狀態(tài)? 能否快速找到 signInFailureLabel 什么時(shí)候顯示/隱藏? 在這個(gè)相對(duì)簡(jiǎn)單的例子中可能只需要2-3分鐘來(lái)回答這個(gè)問(wèn)題摊滔。對(duì)于復(fù)雜的例子阴绢,如果是相同類型的,則需要花費(fèi)相當(dāng)長(zhǎng)的時(shí)間艰躺。

使用ReactiveCocoa會(huì)讓應(yīng)用程序的的邏輯編的簡(jiǎn)潔呻袭。接下來(lái)開始使用。

添加ReactiveCocoa框架依賴

添加ReactiveCocoa最簡(jiǎn)單的方式就是使用 CocoaPods 如果你沒(méi)用過(guò)CocoaPods腺兴,請(qǐng)先去看看 CocoaPods簡(jiǎn)介 這篇文章(或參照CocoaPods的安裝和使用(一))左电。 至少通過(guò)文章中的步驟將CocoaPods初始化完成,你才能安裝ReactiveCocoa页响。

如果由于某種原因你不想使用CocoaPods你仍然可以使用ReactiveCocoa篓足,只需按照GitHub上的文檔中的 導(dǎo)入ReactiveCocoa步驟
詳細(xì)的手導(dǎo)入步驟不在這里做詳細(xì)贅述, 建議去看關(guān)于ReactiveCocoa手動(dòng)導(dǎo)入的教程闰蚕。

對(duì)于RAC栈拖,個(gè)人建議還是使用CocoaPods導(dǎo)入。

打開 Terminal(終端) 進(jìn)到工程目錄
$ pod init
$ open -a Xcode Podfile

在Podfile中添加 pod 'ReactiveCocoa', '~> 4.0.4-alpha-4' 框架

注意: Swift文件使用CocoaPods導(dǎo)入, 需要加 use_frameworks!
因此將Podfile中的 use_frameworks!取消注釋

ReactiveCocoa導(dǎo)入

然后執(zhí)行下載過(guò)程
$ pod install --verbose --no-repo-update

導(dǎo)入完成后的工程目錄

Time To Play

正如介紹中提到的没陡,ReactiveCocoa為處理您的應(yīng)用程序中發(fā)生的事件的不同流的標(biāo)準(zhǔn)接口涩哟。在ReactiveCocoa術(shù)語(yǔ),這些被稱為信號(hào)盼玄,并且由RACSignal類表示贴彼。

打開應(yīng)用的初始的視圖控制器, ViewController.m埃儿,并在頭部引入ReactiveCocoa的頭文件器仗。

#import <ReactiveCocoa/ReactiveCocoa.h>

暫時(shí)不要修改ViewDidLoad中其他的代碼,在ViewDidLoad的末尾添加一些方法童番,隨便做一些測(cè)試:

[self.userNameText.rac_textSignal subscribeNext:^(id x) {
    NSLog(@"%@", x);
}];

編譯并運(yùn)行程序, 在用戶名的輸入框中精钮,輸入內(nèi)容,查看控制臺(tái)打印結(jié)果:

控制臺(tái)打印結(jié)果

從上面的打印結(jié)果剃斧,你可以看出每次修改textfield中的文本時(shí), 該Block塊中的代碼都會(huì)執(zhí)行轨香。沒(méi)有target-action, 沒(méi)有delegate, 只有信號(hào)和blocks。

ReactiveCocoa信號(hào)(由RACSignal表示)發(fā)送事件流給他們的用戶悯衬。有三種類型的事件知道:next弹沽,error和completed檀夹。在出現(xiàn)error或者信號(hào)completed之前, 一個(gè)信號(hào)能送任意數(shù)量的事件。在本節(jié)中策橘,你會(huì)專注于next event炸渡。請(qǐng)務(wù)必閱讀第二部分時(shí),它可用來(lái)了解error和completed的事件丽已。

RACSignal包含很多方法蚌堵,用于訂閱這些不同的事件類型。每個(gè)方法都需要一個(gè)或多個(gè)Block沛婴,當(dāng)事件發(fā)生時(shí)吼畏,在你Block中的邏輯則會(huì)執(zhí)行。在這種情況下嘁灯,可以看出 subscribeNext: 方法被用于提供一個(gè)Block給每一個(gè)將要執(zhí)行的 next event泻蚊。
ReactiveCocoa 框架使用了許多Category給UIKit框架中的一些基本控件添加了許多信號(hào)的方法,因此你可以訂閱他們的事件丑婿。這就是textfield屬性中rac_textSingal的來(lái)源性雄。

原理就介紹到這里,接下來(lái)開始使用ReactiveCocoa去為你做一些事情羹奉。

ReactiveCocoa有許多操作秒旋,你可以用它們來(lái)操縱事件流。例如诀拭,你只讓3個(gè)字符以上的用戶名有效迁筛。你可以實(shí)現(xiàn) filter(過(guò)濾) 這個(gè)操作。在之前的ViewDidLoad添加如下代碼:

[[self.userNameText.rac_textSignal filter:^BOOL(id value) {
    NSString *text = value;
    return text.length > 3;
}] subscribeNext:^(id x) {
    NSLog(@"%@", x);
}];

運(yùn)行之后耕挨,然后鍵入一些文本到textField细卧,你會(huì)發(fā)現(xiàn)。他只會(huì)打印textField長(zhǎng)度大于3的字符串俗孝。


監(jiān)聽圖.gif

你在這里創(chuàng)建了一個(gè)非常簡(jiǎn)單的事件流酒甸。它就是反應(yīng)式編程的本質(zhì)魄健,通過(guò)數(shù)據(jù)流來(lái)表達(dá)應(yīng)用程序的功能赋铝。

下面這張圖片可以幫助你更好的理解數(shù)據(jù)流向:

filter 過(guò)濾數(shù)據(jù)流

從上面的圖中可以看出,rac_textSignal是事件的最初來(lái)源沽瘦。數(shù)據(jù)流通過(guò)過(guò)濾器時(shí)革骨,僅允許字符串的長(zhǎng)度是大于三的事件進(jìn)傳遞。在事件流的最后一步是 subscribeNext: 在這里你可以打印事件的值析恋。

值得一提的一點(diǎn)是良哲,該filter過(guò)濾器的返回值也是一個(gè)RACSignal(即返回值為一個(gè)信號(hào))。你可以通過(guò)如下分步代碼來(lái)理解數(shù)據(jù)流向的具體步驟:

RACSignal *signal = self.userNameText.rac_textSignal;
RACSignal *filterSignal = [signal filter:^BOOL(id value) {
     NSString *text = value;
     return text.length > 3;
}];
    
[filterSignal subscribeNext:^(id x) {
     NSLog(@"%@", x);
}];

一個(gè)RACSignal的每個(gè)操作的返回值也是RACSignal助隧,因此筑凫,它被稱為 流式接口 (fluent interface)。這樣的特征允許你構(gòu)建事件流,而不需要考慮每一步都使用局部變量巍实。

注意: ReactiveCocoa使用了大量的Block滓技。如果你新學(xué)習(xí)Block,你可能需要閱讀蘋果官方的Blocks Programming Topics棚潦。而如果像我一樣令漂,已經(jīng)熟悉了Blocks,但卻發(fā)現(xiàn)很難記住丸边,你可以去看看很有趣的一個(gè)網(wǎng)站How Do I Declare A Block in Objective-C? (經(jīng)測(cè)試該鏈接是正常運(yùn)作的)

類型變換

剛剛我們將之前的代碼分割成了多個(gè)RACSignal叠必,那現(xiàn)在將其改回之前的流式語(yǔ)法:

[[self.userNameText.rac_textSignal filter:^BOOL(id value) {
    NSString *text = value;// 隱式轉(zhuǎn)換
    return text.length > 3;
}] subscribeNext:^(id x) {
    NSLog(@"%@", x);
}];

在上面的代碼中, 注釋部分從id隱式轉(zhuǎn)換成NSString,這樣看起來(lái)不是很優(yōu)雅妹窖。幸運(yùn)的是纬朝,傳遞給該Block中的值就是一個(gè)NSString,你可以更改參數(shù)類型本身骄呼。更新你的代碼如下:

[[self.userNameText.rac_textSignal filter:^BOOL(NSString *text) {
    return text.length > 3;
}] subscribeNext:^(id x) {
    NSLog(@"%@", x);
}];

編譯并運(yùn)行玄组,確保沒(méi)有任何問(wèn)題。

什么是一個(gè)事件谒麦?

到目前為止俄讹,本教程描述了不同的事件類型,但并沒(méi)有詳細(xì)說(shuō)明這些事件的結(jié)構(gòu)绕德。有趣的是患膛,一個(gè)事件絕對(duì)可以包含任何事情!

通過(guò)下面這個(gè)例子耻蛇,你可以將另一個(gè)操作添加到事件流踪蹬。添加如下代碼到你的ViewDidLoad:

[[[self.userNameText.rac_textSignal map:^id(NSString *text) {
    return @(text.length);
}] filter:^BOOL(NSNumber *value) {
    return [value integerValue] > 3;
}] subscribeNext:^(id x) {
    NSLog(@"%@", x);
}];

如果你編譯運(yùn)行,你會(huì)發(fā)現(xiàn)打印的是文本的長(zhǎng)度:

map.gif

新添加的map(映射)操作為改變事件數(shù)據(jù)提供了Block塊臣咖。它將接收到的事件跃捣,通過(guò)執(zhí)行Block塊所得的返回值提供給下一個(gè)事件。在上面的代碼中夺蛇,map的Block返回了取出的NSString文字的長(zhǎng)度, 這使得下一個(gè)事件的值則為NSNumber類型疚漆。

對(duì)于它如何工作的請(qǐng)看下面這張圖片:

Filter And Map

正如你所看到的一樣,所有這一切跟著map的操作進(jìn)行改變, 現(xiàn)在接收到的是NSNumber類型的對(duì)象刁赦。你可以使用 map 操作去將你接收到的數(shù)據(jù)轉(zhuǎn)換成你喜歡的類型, 只要他是一個(gè)對(duì)象類型娶聘。

注意: 在上面的例子中text.length屬性的類型是NSUInteger。為了用它作為事件的內(nèi)容甚脉,它必須被裝箱丸升。幸運(yùn)的是Objective-C的文字語(yǔ)法中提供了字面量 - @(text.length)。

這些足夠開始編寫代碼了! 現(xiàn)在是時(shí)候使用目前學(xué)到的概念更新ReactivePlayground應(yīng)用程序∥保現(xiàn)在你可以刪除所有已經(jīng)添加的代碼了狡耻。

創(chuàng)建有效狀態(tài)的Signals(信號(hào))

首先墩剖,先創(chuàng)建倆個(gè)信號(hào), 表示用戶名密碼是否有效。添加以下內(nèi)容到ViewController.m中:

RACSignal *validUsernameSignal = [self.userNameText.rac_textSignal
     map:^id(NSString *text) {
         return @([self isValidUsername:text]); 
}];
    
RACSignal *validPasswordSignal = [self.passWordText.rac_textSignal
     map:^id(NSString *text) {
         return @([self isValidPassword:text]);
}];

正如你所看到的, 上述代碼通過(guò)map函數(shù)將 rac_textSignal 所生成的字符串類型的值轉(zhuǎn)換為了 NSNumber 類型的 BOOL 值夷狰。

接下來(lái), 轉(zhuǎn)變信號(hào)時(shí)涛碑,可以給textField提供相應(yīng)的背景顏色。簡(jiǎn)單來(lái)說(shuō)孵淘,就是你訂閱這個(gè)信號(hào)并使用信號(hào)的結(jié)果來(lái)更新textFiled的背景顏色蒲障。你可以像下面這樣寫:

 [[validPasswordSignal map:^id(NSNumber *passwordValid) {
        return [passwordValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor];
 }] subscribeNext:^(UIColor *color) {
        self.passWordText.backgroundColor = color;
}];

(請(qǐng)不要添加這段代碼到你的工程中, 因?yàn)檫€有更優(yōu)雅的方案!)

理論上指派的信號(hào)輸出的值會(huì)改變textField的backgroundColor屬性。然而瘫证,這段代碼的方案是比較low的揉阎。

幸運(yùn)的是, ReactiveCocoa有一個(gè)宏, 可以將這段代碼表現(xiàn)的更優(yōu)雅。在viewDidLoad中添加倆個(gè)信號(hào), 代碼如下:

RAC(self.userNameText, backgroundColor) = [validUsernameSignal map:^id(NSNumber *usernameValid) {
        return [usernameValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor];
}];
RAC(self.passWordText, backgroundColor) = [validPasswordSignal map:^id(NSNumber *passwordValid) {
        return [passwordValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor];
}];

RAC的宏會(huì)將信號(hào)輸出的值賦給對(duì)象的屬性背捌。它包含倆個(gè)參數(shù)毙籽,參數(shù)一 : 需要設(shè)置的屬性值的對(duì)象, 參數(shù)二 : 要賦值的屬性名稱毡庆。每個(gè)信號(hào)發(fā)送下一個(gè)事件時(shí), 傳遞的值就會(huì)被賦給指定的屬性坑赡。

這是一個(gè)非常優(yōu)雅的解決方案,你認(rèn)為呢么抗?

在你編譯和運(yùn)行之前毅否,做最后一件事。找到updateUIState方法并刪除掉以下倆行內(nèi)容:

self.userNameText.backgroundColor = self.usernameIsValid ? [UIColor whiteColor] : [UIColor yellowColor];
self.passWordText.backgroundColor = self.passwordIsValid ? [UIColor whiteColor] : [UIColor yellowColor];

這樣就清除掉了非RAC的代碼蝇刀。

編譯并運(yùn)行應(yīng)用程序螟加。你會(huì)發(fā)現(xiàn)textField的內(nèi)容在無(wú)效時(shí),是高亮的吞琐;在有效時(shí)捆探,是透明的。

看起來(lái)效果不錯(cuò)站粟,如果當(dāng)前的邏輯以圖形化來(lái)表示的話黍图,如下圖。在下面你可以看到把倆個(gè)信號(hào)形容成倆個(gè)通道奴烙,倆個(gè)通道做了相同的事情助被。首先,信號(hào)通過(guò)map方法映射成判斷是否有效的BOOL值缸沃,再通過(guò)map方法通過(guò)二次映射將BOOL值轉(zhuǎn)成UIColor恰起,通過(guò)UIColor決定textField的背景顏色修械。

二次映射 map.png

看到這里趾牧,你是否有疑問(wèn)為何要?jiǎng)?chuàng)建倆個(gè)獨(dú)立的validPasswordSignal和validUsernameSignal信號(hào),而不是倆個(gè)輸入框公用一個(gè)信號(hào)呢肯污?想知道答案翘单,接著往下看吨枉!

信號(hào)結(jié)合

在當(dāng)前這個(gè)App中,登錄按鈕在用戶名輸入框和密碼輸入框都有效時(shí), 才被顯示『逦撸現(xiàn)在是時(shí)候改成 響應(yīng)式 了!

當(dāng)前的代碼已經(jīng)具有判斷用戶名和密碼字段是否有效的功能貌亭,并且是一個(gè)可以返回BOOL類型值的信號(hào); 分別是 validUsernameSignalvalidPasswordSignal。接下來(lái)的任務(wù)就是將這倆個(gè)信號(hào)結(jié)合在一起來(lái)決定button是否可以點(diǎn)擊认臊。

在viewDidLoad的末尾處添加如下代碼:

RACSignal *signUpActiveSignal =
[RACSignal combineLatest:@[validUsernameSignal, validPasswordSignal] reduce:^id(NSNumber *usernameValid, NSNumber *passwordValid) {
        return @([usernameValid boolValue] && [passwordValid boolValue]);
}];

上面的代碼使用了combineLatest: reduce: 方法, 通過(guò)結(jié)合validUsernameSignal 和 validPasswordSignal所發(fā)出的結(jié)果圃庭,從而生成一個(gè)新的信號(hào)。每次當(dāng)這倆個(gè)源信號(hào)中任意一個(gè)值改變時(shí), reduce block會(huì)執(zhí)行, 并將倆個(gè)源信號(hào)的值組合作為新的信號(hào)的返回值失晴。

注意: RACSignal的 combinLatest 方法能結(jié)合任意數(shù)量的信號(hào)剧腻, 并且reduce block中的每一個(gè)參數(shù)都是對(duì)應(yīng)源信號(hào)的(combinLatest中的信號(hào)順序, 與參數(shù)順序相同)。
ReactiveCocoa中包含一個(gè)實(shí)用的工具類, RACBlockTrampoline可以在內(nèi)部處理reduce block內(nèi)部變量參數(shù)列表涂屁。事實(shí)上, 有很多隱藏在ReactiveCocoa實(shí)現(xiàn)中的實(shí)用的小技巧书在,非常值得你去學(xué)習(xí)研究。

現(xiàn)在你有一個(gè)非常合適的信號(hào)添加到viewDidLoad結(jié)尾處拆又。將這個(gè)信號(hào)與Button的enabled屬性綁定在一起:

[signUpActiveSignal subscribeNext:^(NSNumber *signupActive) {
    self.signIn.enabled = [signupActive boolValue];
}];

在運(yùn)行代碼之前儒旬,我們需要花點(diǎn)時(shí)間來(lái)刪除一些沒(méi)用的代碼。刪除文件頂部的下面?zhèn)z個(gè)屬性:

@property (nonatomic, assign) BOOL passwordIsValid;
@property (nonatomic, assign) BOOL usernameIsValid;

從最接近viewDidLoad頂部的位置帖族, 移除掉以下代碼:

[self.userNameText addTarget:self action:@selector(usernameTextFieldChanged) forControlEvents:UIControlEventEditingChanged];
[self.passWordText addTarget:self action:@selector(passwordTextFieldChanged) forControlEvents:UIControlEventEditingChanged];

還需要?jiǎng)h除 updateUIState, usernameTextFieldChanged和passwordTextFieldChanged方法栈源。

最后,確保從viewDidLoad中刪除updateUIState方法的調(diào)用竖般。

如果你編譯運(yùn)行凉翻,檢查登錄按鈕的狀態(tài)。如果按鈕是可用的捻激,說(shuō)明userNameText和passWordText都是有效狀態(tài)制轰。和以前的效果一樣。

更新應(yīng)用程序邏輯后胞谭,執(zhí)行流程如下圖:


執(zhí)行流程.png

以上說(shuō)明了幾個(gè)非常重要的概念垃杖,可以使用ReactiveCocoa去執(zhí)行一些非常重量級(jí)的任務(wù)

  • 拆分 - 信號(hào)可以有多個(gè)訂閱者,也可以作為多個(gè)后續(xù)事件流步驟的源丈屹。另外调俘,在上圖中,請(qǐng)注意指示userNameText和passWordText是否有效的信號(hào)旺垒,被分別用于了不同的地方彩库。
  • 聚合 - 多個(gè)信號(hào)可以被組合,以用來(lái)創(chuàng)建新的信號(hào)先蒋。在這種情況下骇钦,倆個(gè)BOOL信號(hào)用做合并。然而竞漾,你可以通過(guò)結(jié)合信號(hào)發(fā)出任意值類型的信號(hào)眯搭。

這些改變的結(jié)果是應(yīng)用程序不用再寫私有屬性用來(lái)表明倆個(gè)textField的當(dāng)前有效狀態(tài)窥翩。你會(huì)發(fā)現(xiàn),這是你采用響應(yīng)式編程與以往模式主要不同的地方之一 --- 你不需要使用實(shí)例變量來(lái)追蹤瞬時(shí)的狀態(tài)鳞仙。

響應(yīng)式登錄

該應(yīng)用目前使用如上圖所示的響應(yīng)式事件流來(lái)管理文本框和按鈕的狀態(tài)寇蚊。不過(guò),按下按鈕操作仍然在使用action做響應(yīng)棍好,所以下一步是使用 響應(yīng)式 更換剩下的應(yīng)用程序的邏輯仗岸。

在ViewController.m中, SignIn按鈕的Touch Up Inside事件通過(guò)StoryBoard與signInTouchUp方法進(jìn)行關(guān)聯(lián)。如果過(guò)你想用響應(yīng)式替代借笙,首先你需要先斷開當(dāng)前故事版與action的連線爹梁。

打開Main.storyboard, 找到Sign In按鈕, 按住Ctrl鍵單擊按鈕,會(huì)彈出outlet/action連接的界面提澎,找到對(duì)應(yīng)的action連接姚垃,點(diǎn)擊x刪除掉對(duì)應(yīng)的連接。下圖很直觀的顯示出來(lái)在哪里可以刪除按鈕的action:

刪除登錄按鈕事件.png

你已經(jīng)看到了ReactiveCocoa框架為標(biāo)準(zhǔn)的UIKit框架的controls添加了屬性和方法盼忌。到現(xiàn)在為止积糯,我們已經(jīng)使用了rac_textSignal, 發(fā)射事件時(shí),文本的變化谦纱。為了更好的處理事件看成,你需要使用ReactiveCocoa中其他的給UIKit添加的controls中的方法--- rac_signalForControlEvents。

回到ViewController.m中, 在viewDidLoad結(jié)尾處添加如下代碼:

[[self.signIn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
        NSLog(@"button clicked");
}];

上述代碼通過(guò)創(chuàng)建一個(gè)按鈕的UIControlEventTouchUpInside事件信號(hào), 并對(duì)該信號(hào)進(jìn)行訂閱跨嘉,使每個(gè)這個(gè)事件被觸發(fā)生時(shí)川慌,都有Log。

編譯運(yùn)行祠乃,驗(yàn)證是否與我們所想象的那樣一致梦重,會(huì)出現(xiàn)消息Log。注意亮瓷,確保用戶名和密碼都是有效時(shí), 按鈕才可以點(diǎn)擊琴拧。所以在點(diǎn)擊按鈕之前,先輸入一些文字到倆個(gè)textField中嘱支。

輸入完畢蚓胸,按鈕有效后。點(diǎn)擊幾次按鈕除师,你可以看到Xcode控制臺(tái)中會(huì)出現(xiàn)如下打印信息:


點(diǎn)擊時(shí)觸發(fā)的打印效果

現(xiàn)在沛膳,這個(gè)按鈕已經(jīng)具有點(diǎn)擊觸發(fā)事件的信號(hào)。下一步就是把該事件與登錄流程組合起來(lái)汛聚。這就出現(xiàn)了問(wèn)題-但這是好的锹安。打開ReactiveManager.h,看看內(nèi)部的代碼:

// 實(shí)際項(xiàng)目可根據(jù)請(qǐng)求結(jié)果決定
typedef void(^SignInResponse)(BOOL);
@interface ReactiveManager : NSObject

/**
 *  登錄的方法
 *
 *  @param username      用戶名
 *  @param password      密碼
 *  @param completeBlock 登錄成功后的回調(diào)
 */
+ (void)signInWithUsername:(NSString *)username
                  password:(NSString *)password
                  complete:(SignInResponse)completeBlock;

@end

該服務(wù)需要用戶名,密碼和完成的 Block 作為參數(shù)八毯。當(dāng)?shù)卿洺晒蚴r(shí)搓侄,Block 會(huì)執(zhí)行瞄桨。你可以在按鈕點(diǎn)擊事件 subscribeNext:Blcok 里直接調(diào)用這個(gè)方法话速,但是這么做會(huì)有些不合適。你可以直接使用 ReactiveCocoa 來(lái)重寫這些代碼芯侥。

注意: 本教程依賴的是一個(gè)虛擬的服務(wù)泊交,這樣并沒(méi)有對(duì)外界的API產(chǎn)生依賴。但是柱查,遇到了新的問(wèn)題廓俭,如何在信號(hào)中表示不是用信號(hào)編寫的API。

創(chuàng)建信號(hào)

幸運(yùn)的是唉工,把一個(gè)現(xiàn)有的異步API表示為一個(gè)信號(hào)還是很簡(jiǎn)單的研乒。首先,從ViewController.m中刪除當(dāng)前signInButtonTouched方法淋硝。已經(jīng)不需要這個(gè)邏輯了雹熬,因?yàn)闀?huì)有與之等價(jià)的方法來(lái)替換它。

然后在ViewController.m中添加以下方法

- (RACSignal *)signInSignal {
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [ReactiveManager signInWithUsername:self.userNameText.text password:self.passWordText.text complete:^(BOOL success) {
             [subscriber sendNext:@(success)];
             [subscriber sendCompleted];
         }];
        return nil;
    }];
}

上述方法谣膳,創(chuàng)建了一個(gè)使用用戶名和密碼登錄的信號(hào)「捅ǎ現(xiàn)在我們拆分來(lái)看看。

上面的代碼使用了 RACSignal 信號(hào)的 createSignal: 的方法继谚。描述這個(gè)信號(hào)模塊的是一個(gè) Block 烈菌。當(dāng)這個(gè)信號(hào)有訂閱者時(shí),會(huì)執(zhí)行此 Block 內(nèi)的代碼花履。

該Block的參數(shù)是一個(gè)遵循 RACSubscriber 協(xié)議的 subscriber芽世,協(xié)議中有很多方法可以產(chǎn)生事件。你還可以發(fā)送任意數(shù)量的 next 事件诡壁,當(dāng)調(diào)用 error 或是 complete 時(shí)則會(huì)被終止捂襟。在本案例中,首先發(fā)送了一個(gè) next 事件來(lái)指示登錄是否成功欢峰,然后發(fā)送了一個(gè) complete 的事件葬荷。

這個(gè)Block的返回類型是 RACDisposable 對(duì)象, 它可以讓你執(zhí)行你所需要的清理工作,例如取消訂閱或丟棄纽帖。這個(gè)信號(hào)并不需要清理任何內(nèi)容宠漩,因此,返回 nil懊直。

正如你所看到的扒吁,這是多么簡(jiǎn)單的一個(gè)封裝異步API的信號(hào)。

接下來(lái)室囊,讓我們利用這一信號(hào)雕崩。更新您添加到ViewDidLoad中末尾的代碼魁索,如下所示例:

[[[self.signIn rac_signalForControlEvents:UIControlEventTouchUpInside] map:^id(id value) {
      return [self signInSignal];
}] subscribeNext:^(id x) {
      NSLog(@"Sign in result: %@", x);
}];

上述代碼使用先前的按鈕觸摸信號(hào)并通過(guò) map(映射) 轉(zhuǎn)換成登錄信號(hào)。用戶只需要登錄的結(jié)果盼铁。

編譯運(yùn)行后粗蔚,點(diǎn)擊登錄按鈕,查看Xcode的控制臺(tái)饶火,你會(huì)看到上面代碼的結(jié)果......

......并且結(jié)果和你預(yù)期的并不一樣鹏控。

2016-03-28 21:44:06.713 ReactivePlayground[2956:83740] Sign in result: <RACDynamicSignal: 0x7fa7fbcb8a80> name:

該信號(hào)已經(jīng)執(zhí)行了 subscribeNext: Block, 但是登錄信號(hào)的輸出結(jié)果與我們想要的并不一樣!

通過(guò)下圖可以告訴你,究竟哪里錯(cuò)了:

信號(hào)轉(zhuǎn)變.png

當(dāng)你點(diǎn)擊按鈕時(shí), rac_signalForControlEvents 會(huì)發(fā)出 next 事件肤寝。map(映射) 會(huì)創(chuàng)建并返回登錄信號(hào), 這意味著事件流的下一步會(huì)接收到 RACSignal 信號(hào)当辐。這就是為什么后面打印出來(lái)的是信號(hào),而不是我們想要的結(jié)果鲤看。

以上情況也有時(shí)會(huì)被稱為信號(hào)的信號(hào)缘揪;換句話說(shuō),一個(gè)外信號(hào)內(nèi)包含了一個(gè)內(nèi)信號(hào)义桂。你可以中外部信號(hào) **subscribeNext: ** 的Block中再訂閱信號(hào)找筝,但是這樣會(huì)造成混亂。幸運(yùn)的是澡刹,ReactiveCocoa為這種常見(jiàn)的情況準(zhǔn)備了應(yīng)對(duì)方案呻征。

信號(hào)的信號(hào)

要解決這個(gè)問(wèn)題很簡(jiǎn)單,只要修改 map(映射) 函數(shù)改為 flattenMap函數(shù), 如下圖所示:

[[[self.signIn rac_signalForControlEvents:UIControlEventTouchUpInside] flattenMap:^RACStream *(id value) {
      return [self signInSignal];
}] subscribeNext:^(id x) {
      NSLog(@"Sign in result: %@", x);
}];

此按鈕觸摸事件轉(zhuǎn)換為了登錄信號(hào)罢浇,并且通過(guò)內(nèi)部信號(hào)向外部信號(hào)發(fā)送了正確的結(jié)果陆赋。

map 映射出的值是 信號(hào)本身
flattenMap 映射出的值是 信號(hào)的值

編譯運(yùn)行,看控制臺(tái)結(jié)果嚷闭,現(xiàn)在應(yīng)該輸出登錄成功或者失敗了攒岛。

2016-03-29 15:53:25.639 ReactivePlayground[5861:233703] Sign in result: 0
2016-03-29 15:53:33.791 ReactivePlayground[5861:233703] Sign in result: 1

現(xiàn)在可以做你想做的事了,最后一步就是將登錄是否成功邏輯添加到 subscribeNext 中胞锰,用以下代碼把剛剛的代碼替換掉灾锯。

[[[self.signIn rac_signalForControlEvents:UIControlEventTouchUpInside] flattenMap:^id(id x) {
    return [self signInSignal];
}] subscribeNext:^(NSNumber *signedIn) {
    BOOL success = [signedIn boolValue];
    self.signInFailureLabel.hidden = success;
    if (success) {
          [self performSegueWithIdentifier:@"signInSuccess" sender:self];
    }
}];

通過(guò) subscribeNext: Block取到結(jié)果,通過(guò)該結(jié)果更新signInFailureLabel的可見(jiàn)性嗅榕,并根據(jù)結(jié)果來(lái)決定是否要執(zhí)行 segue 的跳轉(zhuǎn)顺饮。

編譯運(yùn)行,我們?cè)俅慰吹搅四侵豢蓯?ài)的小貓凌那。

LoginSuccess

就現(xiàn)在的應(yīng)用體驗(yàn)而言兼雄,你有沒(méi)有發(fā)什么什么不好的用戶體驗(yàn)?當(dāng)?shù)卿浄?wù)正在進(jìn)行時(shí), 應(yīng)該禁用登錄按鈕帽蝶。這可以避免用戶重復(fù)登錄赦肋。此外,如果登錄失敗了一次,顯示了錯(cuò)誤提示佃乘,應(yīng)當(dāng)再用戶視圖登錄時(shí)隱藏掉囱井。

但是,問(wèn)題來(lái)了趣避,應(yīng)該如何添加這個(gè)邏輯到當(dāng)前代碼中呢庞呕?更改按鈕的啟用狀態(tài),無(wú)法使用 map filter 或其他已知的概念鹅巍。這就可以稱之為 附加操作 了千扶,因?yàn)檫@個(gè)邏輯應(yīng)該在next事件發(fā)生時(shí)執(zhí)行料祠,并不是改變事件本身骆捧。

附加操作

用以下代碼替換之前所寫代碼:

[[[[self.signIn rac_signalForControlEvents:UIControlEventTouchUpInside] doNext:^(id x) {
    self.signIn.enabled = NO;
    self.signInFailureLabel.hidden = YES;
}] flattenMap:^id(id x) {
    return [self signInSignal];
}] subscribeNext:^(NSNumber *signedIn) {
    self.signIn.enabled = YES;
    BOOL success = [signedIn boolValue];
    self.signInFailureLabel.hidden = success;
    if (success) {
       [self performSegueWithIdentifier:@"signInSuccess" sender:self];
    }
}];

你可以看到上面代碼添加了一個(gè) doNext: , 在觸摸事件后添加的。請(qǐng)注意, doNext:并沒(méi)有返回值, 因?yàn)樗皇且粋€(gè)附加操作髓绽,并且事件本身不變敛苇。

在doNext Block中設(shè)置登錄按鈕不可點(diǎn)擊,并隱藏了 signInFailureLabel顺呕。并在 subscribeNext: Block中重新啟用 登錄按鈕枫攀,并根據(jù)結(jié)果決定是否顯示失敗文本。

現(xiàn)在是時(shí)候更新程序執(zhí)行示意圖株茶,包括附加操作:

doNext附加操作

編譯運(yùn)行来涨,并確認(rèn)按鈕啟用是否與預(yù)期相同。

如果和你想的一樣启盛,那么這個(gè)應(yīng)用就大功告成了蹦掐。

如果你在之前沒(méi)有跟上,可以通過(guò)這個(gè)地址下載最終的項(xiàng)目

注意: 在異步執(zhí)行的過(guò)程中禁用按鈕是一個(gè)常見(jiàn)問(wèn)題僵闯,ReactiveCocoa也能作出很好的解決卧抗。RACCommand就封裝了這個(gè)概念,它有個(gè)enabled信號(hào)鳖粟,使您可以將信號(hào)和enabled連接起來(lái)社裆,你也可以試試這個(gè)類。

總結(jié)

希望本教程可以在你自己的應(yīng)用程序中使用ReactiveCocoa時(shí)給你一個(gè)良好的基礎(chǔ)向图。你可以采取一些練習(xí)來(lái)熟悉這些概念泳秀,就像學(xué)習(xí)一門編程語(yǔ)言或編程一樣,一旦你有了良好的基礎(chǔ)榄攀,它就變得很簡(jiǎn)單嗜傅。ReactiveCocoa中的核心就是信號(hào),無(wú)非就是一堆事件流航攒。還有什么能比這個(gè)更簡(jiǎn)單呢磺陡?

隨著逐漸學(xué)習(xí)ReactiveCocoa,我發(fā)現(xiàn)了其中有可以解決很多疑難問(wèn)題的方法。你可以使用本教程案例試試币他,調(diào)整信號(hào)的組合和拆分坞靶。

這里最值得注意的是ReactiveCocoa的主要目標(biāo)是使你的代碼更加簡(jiǎn)潔,更加容易理解蝴悉。如果應(yīng)用程序的邏輯都清晰的表示成事件流彰阴、流式語(yǔ)法,那這個(gè)應(yīng)用具體做了什么就很好理解了拍冠。

在本教程系列的第二部分尿这,你將學(xué)習(xí)更先進(jìn)的語(yǔ)法。比如錯(cuò)誤處理以及如何管理不同線程中執(zhí)行的代碼等高級(jí)語(yǔ)法庆杜。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末射众,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子晃财,更是在濱河造成了極大的恐慌叨橱,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件断盛,死亡現(xiàn)場(chǎng)離奇詭異罗洗,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)钢猛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門伙菜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人命迈,你說(shuō)我怎么就攤上這事贩绕。” “怎么了躺翻?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵丧叽,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我公你,道長(zhǎng)踊淳,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任陕靠,我火速辦了婚禮迂尝,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘剪芥。我一直安慰自己垄开,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布税肪。 她就那樣靜靜地躺著溉躲,像睡著了一般榜田。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上锻梳,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天箭券,我揣著相機(jī)與錄音,去河邊找鬼疑枯。 笑死辩块,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的荆永。 我是一名探鬼主播废亭,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼誓篱,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼丁寄!你這毒婦竟也來(lái)了连躏?” 一聲冷哼從身側(cè)響起拂酣,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎职辨,沒(méi)想到半個(gè)月后蜜宪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡桃漾,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了拟逮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片撬统。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖敦迄,靈堂內(nèi)的尸體忽然破棺而出恋追,到底是詐尸還是另有隱情,我是刑警寧澤罚屋,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布苦囱,位于F島的核電站,受9級(jí)特大地震影響脾猛,放射性物質(zhì)發(fā)生泄漏撕彤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一猛拴、第九天 我趴在偏房一處隱蔽的房頂上張望羹铅。 院中可真熱鬧,春花似錦愉昆、人聲如沸职员。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)焊切。三九已至扮授,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間专肪,已是汗流浹背糙箍。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留牵祟,地道東北人深夯。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像诺苹,于是被迫代替她去往敵國(guó)和親咕晋。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

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