注意: 此文只是自己翻譯學(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ì)映入眼簾肌毅。
現(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!取消注釋
然后執(zhí)行下載過(guò)程
$ pod install --verbose --no-repo-update
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é)果:
從上面的打印結(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的字符串俗孝。
你在這里創(chuàng)建了一個(gè)非常簡(jiǎn)單的事件流酒甸。它就是反應(yīng)式編程的本質(zhì)魄健,通過(guò)數(shù)據(jù)流來(lái)表達(dá)應(yīng)用程序的功能赋铝。
下面這張圖片可以幫助你更好的理解數(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(映射)操作為改變事件數(shù)據(jù)提供了Block塊臣咖。它將接收到的事件跃捣,通過(guò)執(zhí)行Block塊所得的返回值提供給下一個(gè)事件。在上面的代碼中夺蛇,map的Block返回了取出的NSString文字的長(zhǎng)度, 這使得下一個(gè)事件的值則為NSNumber類型疚漆。
對(duì)于它如何工作的請(qǐng)看下面這張圖片:
正如你所看到的一樣,所有這一切跟著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的背景顏色修械。
看到這里趾牧,你是否有疑問(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); 分別是 validUsernameSignal 和 validPasswordSignal。接下來(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í)行流程如下圖:
以上說(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:
你已經(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)如下打印信息:
現(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ò)了:
當(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)的小貓凌那。
就現(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í)行示意圖株茶,包括附加操作:
編譯運(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ǔ)法庆杜。