原文:ReactiveCocoa Tutorial – The Definitive Introduction: Part 2/2
上半部翻譯:ReactiveCocoa教程:上半部【譯】
ReactiveCocoa框架讓你可以在iOS應(yīng)用中使用響應(yīng)式函數(shù)編程(FRP)耘擂。在教程的上半部分你學(xué)會(huì)了如何用發(fā)送事件流的信號(hào)替換標(biāo)準(zhǔn)的動(dòng)作和事件處理邏輯冯键,還有如何對(duì)這些信號(hào)進(jìn)行轉(zhuǎn)換惊豺、拆分和重組。
而在教程的下半部分,你將學(xué)到ReactiveCocoa更深層次的功能兑宇,如:
- 另外兩種事件類型:
error
和complete
- 限流
- 多線程
- 持續(xù)化
- 等等……
事不宜遲末盔,立馬開始吧!
推特即時(shí)搜索
在本教程中你將要開發(fā)的應(yīng)用叫做推特即時(shí)搜索(模仿谷歌即時(shí)搜索的概念)媒吗,一個(gè)在輸入時(shí)即時(shí)更新搜索記錄的推特搜索應(yīng)用。
應(yīng)用的初始項(xiàng)目包含了一些你開始時(shí)需要的基礎(chǔ)的界面和普通代碼乙埃。和教程的上半部分一樣闸英,你需要使用CocoaPods獲取ReactiveCocoa框架并整合到你的項(xiàng)目中锯岖。初始項(xiàng)目已經(jīng)包含了必須的Podfile
文件,所以直接打開終端窗口和執(zhí)行下列命令:
pod install
如果正確執(zhí)行的話甫何,你會(huì)看到相似輸出如下:
Analyzing dependencies
Downloading dependencies
Using ReactiveCocoa (2.1.8)
Generating Pods project
Integrating client project
這回生成一個(gè)Xcode workspace文件:TwitterInstant.xcworkspace
出吹。在Xcode中打開該文件,并確定里面包含了兩個(gè)項(xiàng)目:
- TwitterInstant:應(yīng)用邏輯所在
- Pods:項(xiàng)目的外部引用辙喂,現(xiàn)在包含ReactiveCocoa框架
編譯運(yùn)行趋箩,你會(huì)看到下圖的界面:
先花點(diǎn)時(shí)間熟悉一下應(yīng)用的代碼。這是一個(gè)非常簡(jiǎn)單基于拆分視圖控制器的app(split view controller-based app)加派。左邊的部分是RWSearchFormViewController
叫确,包含了一些通過storyboard添加的UI事件和一個(gè)外聯(lián)的搜索文本框。右邊的部分是RWSearchResultsViewController
芍锦,暫時(shí)只是一個(gè)UITableViewController
的子類竹勉。
打開RWSearchFormViewController.m
文件你就能看到在viewDidLoad
方法中定位了結(jié)果展示控制器,并將它指向resultsViewController
私有屬性娄琉。由于這個(gè)應(yīng)用最主要的邏輯就落在RWSearchFormViewController
上次乓,這個(gè)屬性將有助于為RWSearchResultsViewController
提供搜索的結(jié)果。
校驗(yàn)搜索文本
你首先要做的是驗(yàn)證搜索文本孽水,確保它的長(zhǎng)度大于兩個(gè)字節(jié)票腰。如果你完成了上半部教程的話,這對(duì)你來說應(yīng)該是記憶猶新女气。在RWSearchFormViewController.m
的viewDidLoad
方法下添加以下代碼:
- (BOOL)isValidSearchText:(NSString *)text {
return text.length > 2;
}
這方法簡(jiǎn)單地判斷搜索字符串是否長(zhǎng)于兩個(gè)字節(jié)杏慰。這邏輯簡(jiǎn)單得你可能會(huì)問:“為什么這都要單獨(dú)分離出一個(gè)方法呢?”
現(xiàn)在的邏輯的確相當(dāng)?shù)暮?jiǎn)單炼鞠,但如果將來這校驗(yàn)需要變得更為復(fù)雜呢缘滥?在上面的例子中,你只需要在改變一個(gè)地方就可以了谒主。不止如此朝扼,上面的實(shí)現(xiàn)讓你的代碼可讀性更好,指出了你判斷字符串長(zhǎng)度的原因霎肯。想必我們都遵循著良好的編碼習(xí)慣對(duì)么擎颖?
在文件的頂部導(dǎo)入ReactiveCocoa:
#import <ReactiveCocoa.h>
在同一文件的viewDidLoad
方法末端添加如下代碼:
[[self.searchText.rac_textSignal
map:^id(NSString *text) {
return [self isValidSearchText:text] ?
[UIColor whiteColor] : [UIColor yellowColor];
}]
subscribeNext:^(UIColor *color) {
self.searchText.backgroundColor = color;
}];
不明白這都做了什么?上面的代碼實(shí)現(xiàn)了三件事:
- 獲取了搜索文本框的文本信號(hào)
- 把文本是否有效的校驗(yàn)結(jié)果轉(zhuǎn)換成背景顏色
- 然后在
subscribeNext:
的block中將上一步所得賦值給backgroundColor
屬性
編譯運(yùn)行后能看到當(dāng)搜索文本過短時(shí)观游,文本框會(huì)判斷這為無效輸入搂捧,并把背景顏色變成黃色。
如果用圖表描述的話备典,這個(gè)簡(jiǎn)單的響應(yīng)式管道看起來是這樣的:
每當(dāng)文本發(fā)生改變時(shí)异旧,rac_textSignal
就會(huì)發(fā)送包含當(dāng)前文本內(nèi)容的next
事件。map
方法把文本轉(zhuǎn)化成顏色提佣,然后在subscribeNext:
環(huán)節(jié)中獲取并賦值給文本框的背景顏色吮蛹。
想必你還記得上半部中關(guān)于這一部分內(nèi)容對(duì)嗎?如果不記得拌屏,你可能就需要先停下來潮针,去回顧一下上半部的練習(xí)部分了。
而在添加推特的搜索邏輯前倚喂,這還有一些更加有趣的話題需要提及每篷。
格式化管道
當(dāng)你研究格式化ReactiveCocoa代碼時(shí),慣例是一個(gè)操作對(duì)應(yīng)一行端圈,并垂直對(duì)齊每一個(gè)步驟焦读。
在下圖中,你可以看到一個(gè)在更為復(fù)雜情況下的格式對(duì)齊舱权,這是從上一個(gè)教程中截取出來的:
這讓你更容易看到管道的操作組成矗晃。同時(shí)這精簡(jiǎn)了每個(gè)block中的代碼,任何超過兩行的代碼都應(yīng)該封裝為一個(gè)私有方法宴倍。
但很不幸张症,Xcode并不是太喜歡這種格式化形式,所以你可能需要手動(dòng)與它的自動(dòng)縮格邏輯作斗爭(zhēng)鸵贬!
內(nèi)存管理
考慮一下你添加到TwitterInstant
app的代碼俗他,你是否為你剛創(chuàng)建的管道是如何保持(retained)的感到疑惑?當(dāng)然了阔逼,由于管道并沒有指向一個(gè)變量或者屬性兆衅,它的引用計(jì)數(shù)自然不會(huì)增加,那它隨后是否就會(huì)被直接銷毀呢嗜浮?
匿名構(gòu)造管道是ReactiveCocoa的其中一個(gè)設(shè)計(jì)理念涯保。回顧至今為止你寫的所有響應(yīng)式代碼周伦,這應(yīng)該是顯而易見的夕春。
為了支持這種特性,ReactiveCocoa維系保持了它自己的全局信號(hào)集(global set of signals)专挪。如果信號(hào)有一個(gè)或多個(gè)訂閱者的話及志,信號(hào)就會(huì)被激活。如果所有的訂閱者都給移除了寨腔,該信號(hào)就可以被回收速侈。想知道更多關(guān)于ReactiveCocoa內(nèi)管理這個(gè)過程的內(nèi)容,你可以瀏覽Memory Management文檔(譯注:文檔已失效)迫卢。
這就剩下最后一個(gè)問題了:怎樣取消信號(hào)的訂閱呢倚搬?訂閱在接收到completed
或event
事件后,就會(huì)自動(dòng)移除(你很快就會(huì)學(xué)到更多關(guān)于這部分的內(nèi)容)乾蛤。而要手動(dòng)移除的話可以借助RACDisposable
.
RACSignal
的訂閱方法都返回了一個(gè)RACDisposable
實(shí)例用以在處理方法中手動(dòng)移除訂閱每界。舉一個(gè)基于現(xiàn)有管道的簡(jiǎn)單例子:
RACSignal *backgroundColorSignal =
[self.searchText.rac_textSignal
map:^id(NSString *text) {
return [self isValidSearchText:text] ?
[UIColor whiteColor] : [UIColor yellowColor];
}];
RACDisposable *subscription =
[backgroundColorSignal
subscribeNext:^(UIColor *color) {
self.searchText.backgroundColor = color;
}];
// at some point in the future ...
[subscription dispose];
可能在實(shí)際中你很少會(huì)這樣做捅僵,但知道這么一個(gè)可行操作還是很有價(jià)值的。
注意:相對(duì)應(yīng)的眨层,如果你創(chuàng)建了一個(gè)管道但不曾對(duì)其訂閱庙楚,這管道里的代碼,包括像
doNext:
這樣的副作用都永遠(yuǎn)不會(huì)執(zhí)行趴樱。
避免引用循環(huán)
ReactiveCocoa已經(jīng)在背后作了很多精妙的處理馒闷,這意味著你并不需要擔(dān)心太多關(guān)于信號(hào)內(nèi)存管理的細(xì)節(jié)。但這還是有一個(gè)重要的內(nèi)存相關(guān)問題你需要關(guān)心的叁征。
看看你剛才添加的響應(yīng)式代碼:
[[self.searchText.rac_textSignal
map:^id(NSString *text) {
return [self isValidSearchText:text] ?
[UIColor whiteColor] : [UIColor yellowColor];
}]
subscribeNext:^(UIColor *color) {
self.searchText.backgroundColor = color;
}];
在subscribeNext:
的block中使用self
以獲取文本框的引用纳账。block從會(huì)從封閉作用域中捕獲并持有了相關(guān)值,因而當(dāng)self
和信號(hào)中存在強(qiáng)引用時(shí)捺疼,就會(huì)導(dǎo)致引用循環(huán)疏虫。這會(huì)不會(huì)導(dǎo)致問題取決于self
對(duì)象的的生命周期。如果像這個(gè)例子一樣帅涂,它的生命周期貫穿整個(gè)應(yīng)用议薪,就并不構(gòu)成問題。但這在更加復(fù)雜的應(yīng)用中是很少出現(xiàn)的媳友。
為了避免潛在的引用循環(huán)斯议,蘋果的官方文檔Working With Blocks推薦對(duì)self
使用弱引用。你可以在現(xiàn)有的代碼中作如下實(shí)現(xiàn):
__weak RWSearchFormViewController *bself = self; // Capture the weak reference
[[self.searchText.rac_textSignal
map:^id(NSString *text) {
return [self isValidSearchText:text] ?
[UIColor whiteColor] : [UIColor yellowColor];
}]
subscribeNext:^(UIColor *color) {
bself.searchText.backgroundColor = color;
}];
上面的代碼中bself
是對(duì)self
的引用醇锚,__weak
標(biāo)記讓該引用變?yōu)槿跻煤哂W⒁?code>subscribeNext:的block中現(xiàn)在使用的就是bself
變量了,這看起來實(shí)在相當(dāng)不美觀焊唬!
ReactiveCocoa框架提供了一個(gè)可以代替上面代碼的小竅門恋昼。在文件頂端添加導(dǎo)入如下:
#import "RACEXTScope.h"
然后替換剛才的代碼如下:
@weakify(self)
[[self.searchText.rac_textSignal
map:^id(NSString *text) {
return [self isValidSearchText:text] ?
[UIColor whiteColor] : [UIColor yellowColor];
}]
subscribeNext:^(UIColor *color) {
@strongify(self)
self.searchText.backgroundColor = color;
}];
代碼中的@weakify
和@strongify
是定義在Extended Objective-C庫的宏,這也已經(jīng)包含在ReactiveCocoa框架中赶促。@weakify
宏創(chuàng)建了弱應(yīng)用的影子變量(shadow variables)(如果你需要多個(gè)弱引用液肌,你可以傳入多個(gè)變量),@strongify
宏則使用先前傳到@weakify
的變量創(chuàng)建強(qiáng)引用鸥滨。
注意:如果你對(duì)
@weakify
和@strongify
的具體操作感到好奇嗦哆,你可以在Xcode中選擇Product -> Perform Action -> Preprocess “RWSearchForViewController”。這會(huì)對(duì)視圖控制器進(jìn)行預(yù)處理婿滓,展開所有的宏讓你看到最終的輸出老速。
最后需要注意的是,在block中使用實(shí)例變量時(shí)也要小心凸主。這也會(huì)導(dǎo)致block對(duì)self進(jìn)行強(qiáng)引用橘券。你可以打開編譯器警告,當(dāng)你的代碼導(dǎo)致這種問題時(shí)去提醒你。在項(xiàng)目的build settings中搜索retain旁舰,找到如下的設(shè)置:
好了锋华,恭喜你終于熬過了理論知識(shí)!現(xiàn)在你已經(jīng)為最有趣的部分做好了充足的準(zhǔn)備:為應(yīng)用添加真正的功能鬓梅!
注意:看過上一個(gè)教程的敏銳讀者想必已經(jīng)發(fā)現(xiàn)在這管道中可以使用
RAC
宏去替代subscribeNext:
供置。如果你已經(jīng)發(fā)現(xiàn)了谨湘,那就改改上面的代碼并獎(jiǎng)勵(lì)自己一朵小紅花吧绽快!
連接推特
你將要使用Social Framework
在你的應(yīng)用中搜索推特,使用Accounts Framework
去獲取推特的授權(quán)紧阔。想要獲取更多關(guān)于Social Framework
的信息坊罢,可以查看iOS 6 by Tutorials這篇介紹這個(gè)框架的文章。
在添加代碼前擅耽,你需要在運(yùn)行本應(yīng)用的模擬器或iPad上輸入你推特的用戶密碼活孩。打開設(shè)置并選中推特選項(xiàng),在屏幕的右方添加你的用戶密碼:
初始項(xiàng)目已經(jīng)添加了所需框架乖仇,所以你只需要導(dǎo)入相關(guān)頭文件憾儒。在RWSearchFormViewController.m
的頂端添加引用如下:
#import <Accounts/Accounts.h>
#import <Social/Social.h>
在引用的下方添加如下枚舉和常量:
typedef NS_ENUM(NSInteger, RWTwitterInstantError) {
RWTwitterInstantErrorAccessDenied,
RWTwitterInstantErrorNoTwitterAccounts,
RWTwitterInstantErrorInvalidResponse
};
static NSString * const RWTwitterInstantDomain = @"TwitterInstant";
你很快就會(huì)用到這些去區(qū)分錯(cuò)誤。
在同一個(gè)文件乃沙,在現(xiàn)有的屬性聲明下添加如下屬性:
@property (strong, nonatomic) ACAccountStore *accountStore;
@property (strong, nonatomic) ACAccountType *twitterAccountType;
ACAccountsStore
類為你的設(shè)備提供了多種可用的社交媒體賬號(hào)的連接途徑起趾,ACAccountType
則類代表了賬戶的具體類型。
在同一文件的viewDidLoad
方法末端添加代碼如下:
self.accountStore = [[ACAccountStore alloc] init];
self.twitterAccountType = [self.accountStore
accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];
以上代碼創(chuàng)建了賬號(hào)的庫(accounts store)和推特的賬號(hào)標(biāo)識(shí)警儒。
當(dāng)應(yīng)用請(qǐng)求連接一個(gè)社交賬戶時(shí)训裆,用戶會(huì)看到一個(gè)彈窗。這是一個(gè)異步操作蜀铲,所以最好將它用信號(hào)封裝起來边琉,以便響應(yīng)式使用。
繼續(xù)添加以下代碼:
- (RACSignal *)requestAccessToTwitterSignal {
// 1 - define an error
NSError *accessError = [NSError errorWithDomain:RWTwitterInstantDomain
code:RWTwitterInstantErrorAccessDenied
userInfo:nil];
// 2 - create the signal
@weakify(self)
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 3 - request access to twitter
@strongify(self)
[self.accountStore
requestAccessToAccountsWithType:self.twitterAccountType
options:nil
completion:^(BOOL granted, NSError *error) {
// 4 - handle the response
if (!granted) {
[subscriber sendError:accessError];
} else {
[subscriber sendNext:nil];
[subscriber sendCompleted];
}
}];
return nil;
}];
}
該方法做了以下操作:
- 定義了一個(gè)錯(cuò)誤记劝,當(dāng)用戶連接遭拒時(shí)發(fā)送变姨。
- 像第一篇文章所說,類方法
createSignal
返回了一個(gè)RACSignal
的實(shí)例厌丑。 - 通過賬戶庫鏈接到推特定欧。此時(shí),用戶會(huì)看到是否允許app連接到他們推特賬號(hào)的提示蹄衷。
- 當(dāng)用戶同意或拒絕了連接忧额,信號(hào)事件就會(huì)發(fā)送。如果用戶同意連接愧口,一個(gè)
next
事件和緊接一個(gè)completed
事件就會(huì)被發(fā)送睦番。如果用戶拒絕了連接一個(gè)error
事件就會(huì)被發(fā)送。
回想一下上半部的教程,一個(gè)信號(hào)可以發(fā)送三種不同類型的事件:
- Next
- Completed
- Error
在信號(hào)的整個(gè)生命周期托嚣,它可能不發(fā)送任何事件巩检,也可能發(fā)送一個(gè)或多個(gè)next
事件然后緊跟一個(gè)completed
事件或者error
事件。
最后為了使用這個(gè)信號(hào)示启,在viewDidLoad
方法末端添加以下代碼:
[[self requestAccessToTwitterSignal]
subscribeNext:^(id x) {
NSLog(@"Access granted");
} error:^(NSError *error) {
NSLog(@"An error occurred: %@", error);
}];
編譯運(yùn)行兢哭,你就能看到下圖這樣的彈出框:
如果你點(diǎn)擊確定按鈕,控制臺(tái)就會(huì)答應(yīng)
subscribeNext:
block中的記錄信息夫嗓,相反迟螺,如果你點(diǎn)擊不允許,error里的代碼塊就會(huì)執(zhí)行并打印響應(yīng)的記錄舍咖。
賬戶管理框架會(huì)記住你的選擇矩父。所以為了測(cè)試兩種情況,你需要在菜單中選擇重置模擬器: iOS Simulator -> Reset Contents and Settings …
排霉。這會(huì)有一點(diǎn)繁瑣窍株,因?yàn)橹刂煤竽氵€需要重新輸入你的推特賬號(hào)密碼!
鏈接信號(hào)
當(dāng)用戶成功連接到他們的推特賬號(hào)(希望如此!),應(yīng)用就要繼續(xù)監(jiān)聽搜索輸入框的改變來搜索推特歇式。
應(yīng)用需要等連接推特的信號(hào)發(fā)送completed
事件户誓,并傳遞給輸入框的信號(hào)。這種連續(xù)的信號(hào)鏈接是相當(dāng)常見的問題,但是ReactiveCocoa對(duì)此有非常優(yōu)雅的解決方案。
替換viewDidLoad
末端現(xiàn)有的管道如下:
[[[self requestAccessToTwitterSignal]
then:^RACSignal *{
@strongify(self)
return self.searchText.rac_textSignal;
}]
subscribeNext:^(id x) {
NSLog(@"%@", x);
} error:^(NSError *error) {
NSLog(@"An error occurred: %@", error);
}];
then
方法會(huì)一直等待直到信號(hào)的completed
事件被發(fā)送,然后轉(zhuǎn)訂閱參數(shù)代碼塊中返回的信號(hào)旦部。這有效的將控制權(quán)從一個(gè)信號(hào)轉(zhuǎn)遞給下一個(gè)信號(hào)。
注意:你已經(jīng)在上一個(gè)管道弱引用過
self
较店,所以不再需要在這個(gè)管道前添加@weakify(self)
了士八。
編譯運(yùn)行并允許連接,你會(huì)看到你在搜索文本框輸入的文本此時(shí)打印在了控制臺(tái):
2014-01-04 08:16:11.444 TwitterInstant[39118:a0b] m
2014-01-04 08:16:12.276 TwitterInstant[39118:a0b] ma
2014-01-04 08:16:12.413 TwitterInstant[39118:a0b] mag
2014-01-04 08:16:12.548 TwitterInstant[39118:a0b] magi
2014-01-04 08:16:12.628 TwitterInstant[39118:a0b] magic
2014-01-04 08:16:13.172 TwitterInstant[39118:a0b] magic!
接下來梁呈,為管道添加一個(gè)過濾操作婚度,將無效的搜索字符串移除掉。在這個(gè)例子中官卡,無效指的就是少于3個(gè)字節(jié)的字符串:
[[[[self requestAccessToTwitterSignal]
then:^RACSignal *{
@strongify(self)
return self.searchText.rac_textSignal;
}]
filter:^BOOL(NSString *text) {
@strongify(self)
return [self isValidSearchText:text];
}]
subscribeNext:^(id x) {
NSLog(@"%@", x);
} error:^(NSError *error) {
NSLog(@"An error occurred: %@", error);
}];
再次編譯運(yùn)行蝗茁,實(shí)際觀察一下過濾效果:
2014-01-04 08:16:12.548 TwitterInstant[39118:a0b] magi
2014-01-04 08:16:12.628 TwitterInstant[39118:a0b] magic
2014-01-04 08:16:13.172 TwitterInstant[39118:a0b] magic!
用圖表說明一下現(xiàn)在的應(yīng)用邏輯,那看起來是這樣的:
應(yīng)用管道從
requestAccessToTwitterSignal
開始寻咒,然后切換到rac_textSignal
哮翘。與此同時(shí),next
事件通過過濾器最終到達(dá)訂閱的block毛秘。第一個(gè)環(huán)節(jié)中發(fā)送的error
事件同樣能夠被同一個(gè)subscribeNext:error:
方法捕獲到饭寺。
現(xiàn)在你已經(jīng)有了一個(gè)發(fā)送搜索文本的信號(hào)阻课,是時(shí)候使用來搜索推特了!你現(xiàn)在享受到其中的樂趣了么艰匙?想必是限煞,畢竟你已經(jīng)大展一番拳腳了!
推特搜索
Social Framework
是使用推特搜索API的一種方式员凝。但是署驻,如你所想,Social Framework
并不是響應(yīng)式的健霹!下一步要做的就是將需要的API方法封裝在一個(gè)信號(hào)中調(diào)用旺上。 你這下應(yīng)該搞明白了吧!
在RWSearchFormViewController.m
中添加下列方法:
- (SLRequest *)requestforTwitterSearchWithText:(NSString *)text {
NSURL *url = [NSURL URLWithString:@"https://api.twitter.com/1.1/search/tweets.json"];
NSDictionary *params = @{@"q" : text};
SLRequest *request = [SLRequest requestForServiceType:SLServiceTypeTwitter
requestMethod:SLRequestMethodGET
URL:url
parameters:params];
return request;
}
這根據(jù)v1.1 REST API標(biāo)準(zhǔn)創(chuàng)建了搜索推特的請(qǐng)求。上面的代碼使用q
搜索參數(shù)用以搜索所有包含搜索關(guān)鍵字的推特骤公。你可以在推特的接口文檔查看更多關(guān)于這個(gè)搜索接口信息抚官,以及其他可以傳遞的有效參數(shù)列表扬跋。
下一步就是基于這個(gè)請(qǐng)求創(chuàng)建信號(hào)阶捆。在同一文件添加下列方法:
- (RACSignal *)signalForSearchWithText:(NSString *)text {
// 1 - define the errors
NSError *noAccountsError = [NSError errorWithDomain:RWTwitterInstantDomain
code:RWTwitterInstantErrorNoTwitterAccounts
userInfo:nil];
NSError *invalidResponseError = [NSError errorWithDomain:RWTwitterInstantDomain
code:RWTwitterInstantErrorInvalidResponse
userInfo:nil];
// 2 - create the signal block
@weakify(self)
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
@strongify(self);
// 3 - create the request
SLRequest *request = [self requestforTwitterSearchWithText:text];
// 4 - supply a twitter account
NSArray *twitterAccounts = [self.accountStore
accountsWithAccountType:self.twitterAccountType];
if (twitterAccounts.count == 0) {
[subscriber sendError:noAccountsError];
} else {
[request setAccount:[twitterAccounts lastObject]];
// 5 - perform the request
[request performRequestWithHandler: ^(NSData *responseData,
NSHTTPURLResponse *urlResponse, NSError *error) {
if (urlResponse.statusCode == 200) {
// 6 - on success, parse the response
NSDictionary *timelineData =
[NSJSONSerialization JSONObjectWithData:responseData
options:NSJSONReadingAllowFragments
error:nil];
[subscriber sendNext:timelineData];
[subscriber sendCompleted];
}
else {
// 7 - send an error on failure
[subscriber sendError:invalidResponseError];
}
}];
}
return nil;
}];
}
分解一下步驟:
- 剛開始,定義了兩種不同的錯(cuò)誤钦听,一個(gè)表示用戶尚未在設(shè)備中添加推特賬戶洒试,另一個(gè)表示查詢過程中發(fā)生的錯(cuò)誤。
- 像之前一樣朴上,創(chuàng)建一個(gè)信號(hào)垒棋。
- 使用上一步你創(chuàng)建的方法根據(jù)提供的搜索關(guān)鍵字創(chuàng)建請(qǐng)求。
- 查詢賬號(hào)庫中第一個(gè)有效的推特賬戶痪宰。如果沒有任何賬戶返回叼架,發(fā)送錯(cuò)誤事件。
- 執(zhí)行請(qǐng)求衣撬。
- 當(dāng)成功返回時(shí)(HTTP返回編碼為200)乖订,轉(zhuǎn)換返回的JSON數(shù)據(jù)并伴隨
next
事件發(fā)送,緊跟發(fā)送一個(gè)completed
事件具练。 - 當(dāng)返回狀態(tài)為不成功時(shí)乍构,發(fā)送一個(gè)
error
事件。
現(xiàn)在就能使用這個(gè)新的信號(hào)了扛点!
在本教程的上半部分你學(xué)會(huì)了如何使用flattenMap
去映射每一個(gè)next
事件為一個(gè)全新的信號(hào)并接著訂閱它「缯冢現(xiàn)在就要再次運(yùn)用這個(gè)方法。更新viewDidLoad
方法內(nèi)末端的管道陵究,在最后添加flattenMap
環(huán)節(jié):
[[[[[self requestAccessToTwitterSignal]
then:^RACSignal *{
@strongify(self)
return self.searchText.rac_textSignal;
}]
filter:^BOOL(NSString *text) {
@strongify(self)
return [self isValidSearchText:text];
}]
flattenMap:^RACStream *(NSString *text) {
@strongify(self)
return [self signalForSearchWithText:text];
}]
subscribeNext:^(id x) {
NSLog(@"%@", x);
} error:^(NSError *error) {
NSLog(@"An error occurred: %@", error);
}];
編譯運(yùn)行眠饮,在搜索框中輸入一些文本。當(dāng)文本達(dá)到或超過3個(gè)字節(jié)時(shí)铜邮,你就能在控制臺(tái)中看到推特的搜索記錄仪召。
下面節(jié)選了一段你會(huì)看到的數(shù)據(jù)樣式:
2014-01-05 07:42:27.697 TwitterInstant[40308:5403] {
"search_metadata" = {
"completed_in" = "0.019";
count = 15;
"max_id" = 419735546840117248;
"max_id_str" = 419735546840117248;
"next_results" = "?max_id=419734921599787007&q=asd&include_entities=1";
query = asd;
"refresh_url" = "?since_id=419735546840117248&q=asd&include_entities=1";
"since_id" = 0;
"since_id_str" = 0;
};
statuses = (
{
contributors = "<null>";
coordinates = "<null>";
"created_at" = "Sun Jan 05 07:42:07 +0000 2014";
entities = {
hashtags = ...
signalForSearchText:
方法發(fā)送的error
事件同樣能給subscribeNext:error:
接收到。你可能已經(jīng)記住了這點(diǎn),但相比你更可能希望親手試驗(yàn)一下返咱!
在模擬器中打開設(shè)置并選中的的推特賬戶钥庇,然后點(diǎn)擊刪除賬戶按鈕:
重運(yùn)行應(yīng)用,應(yīng)用仍然會(huì)獲取用戶推特賬號(hào)的授權(quán)咖摹,盡管已經(jīng)現(xiàn)在已經(jīng)沒有有效的賬號(hào)了评姨。因此
signalForSearchText
方法會(huì)發(fā)送一個(gè)錯(cuò)誤,在控制臺(tái)打佑┣纭:
2014-01-05 07:52:11.705 TwitterInstant[41374:1403] An error occurred: Error
Domain=TwitterInstant Code=1 "The operation couldn’t be completed. (TwitterInstant error 1.)"
Code=1
指明了這是一個(gè)RWTwitterInstantErrorNoTwitterAccounts
錯(cuò)誤吐句。在生產(chǎn)環(huán)境的應(yīng)用中,你會(huì)希望把錯(cuò)誤編碼轉(zhuǎn)換成更有意義的形式店读,而不是只在控制臺(tái)打印記錄嗦枢。
這引出了一個(gè)關(guān)于error
事件的要點(diǎn);當(dāng)信號(hào)發(fā)出一個(gè)錯(cuò)誤時(shí)屯断,它會(huì)直接傳遞給處理錯(cuò)誤的block文虏。這是一個(gè)異常的處理流。
提醒:想試驗(yàn)訪問推特失敗時(shí)的異常處理流的話殖演,有一個(gè)小竅門氧秘,把請(qǐng)求入?yún)⒏臑闊o效數(shù)據(jù)就可以了!
多線程
相信你已經(jīng)迫不及待要將搜索結(jié)果的JSON輸出轉(zhuǎn)化為UI了趴久,但在那之前你還需要做一件事丸相。而為了明確你要做的事,你還需要做一點(diǎn)探索彼棍。
在subscribeNext:error:
方法中如下的位置添加斷點(diǎn):
重運(yùn)行應(yīng)用灭忠,如果有必要的話重新輸入你的推特賬號(hào)密碼,然后在搜索框中輸入一些內(nèi)容座硕。當(dāng)執(zhí)行到斷點(diǎn)時(shí)你會(huì)看到下圖相似的景象:
注意調(diào)試中的代碼并不是在主線程
Thread 1
中執(zhí)行的弛作。謹(jǐn)記你只能在主線程中更新UI界面;所以如果你希望在UI界面中更新UI的話坎吻,你需要切換執(zhí)行線程缆蝉。
這體現(xiàn)了ReactiveCocoa框架一個(gè)非常重要的特點(diǎn)。上面的操作是在信號(hào)開始發(fā)送信號(hào)的線程中執(zhí)行的瘦真。在管道的其他環(huán)節(jié)添加斷點(diǎn)?刊头,你可能會(huì)驚訝地發(fā)現(xiàn)他們并不在同一個(gè)線程中執(zhí)行!
所以要如何更新UI界面呢诸尽?傳統(tǒng)的做法是使用操作隊(duì)列(更多的細(xì)節(jié)可以看本站的另一篇文章 How To Use NSOperations and NSOperationQueues)原杂,但是ReactiveCocoa提供了一個(gè)更加簡(jiǎn)便的解決方法。
在管道的flattenMap:
方法后添加deliverOn:
方法如下:
[[[[[[self requestAccessToTwitterSignal]
then:^RACSignal *{
@strongify(self)
return self.searchText.rac_textSignal;
}]
filter:^BOOL(NSString *text) {
@strongify(self)
return [self isValidSearchText:text];
}]
flattenMap:^RACStream *(NSString *text) {
@strongify(self)
return [self signalForSearchWithText:text];
}]
deliverOn:[RACScheduler mainThreadScheduler]]
subscribeNext:^(id x) {
NSLog(@"%@", x);
} error:^(NSError *error) {
NSLog(@"An error occurred: %@", error);
}];
現(xiàn)在重運(yùn)行app您机,任意輸入一點(diǎn)內(nèi)容讓app運(yùn)行到斷點(diǎn)處穿肄。你會(huì)看到subscribeNext:error:
中控制臺(tái)打印的代碼現(xiàn)在在主線程中執(zhí)行了:
什么年局?這不過簡(jiǎn)單的調(diào)整了一下代碼就能改變事件流的執(zhí)行線程?這實(shí)在太棒了咸产!你可以更新你的UI界面了矢否!
注意:如果你關(guān)注一下
RACScheduler
類你就能看到其提供了相當(dāng)多的選擇來實(shí)現(xiàn)不同的線程優(yōu)先級(jí)和管道延遲處理。
現(xiàn)在是時(shí)候展現(xiàn)這些推特了脑溢!
更新UI界面
打開RWSearchResultsViewController.h
你能看到里面已經(jīng)定義displayTweets:
了方法僵朗,用以為右手邊的視圖控制器渲染提供的推特列表。里面的實(shí)現(xiàn)非常簡(jiǎn)單屑彻,只是標(biāo)準(zhǔn)的UITableView
數(shù)據(jù)源處理验庙。displayTweets:
方法的唯一入?yún)⑹且粋€(gè)裝載RWTweet
實(shí)例的NSArray
。初始項(xiàng)目也已經(jīng)為你提供了RWTweet
對(duì)象模型社牲。
subscibeNext:error:
中接收的是在signalForSearchWithText:
方法中從JSON轉(zhuǎn)換成的NSDictionary
類型數(shù)據(jù)粪薛。所以你怎樣才能知道字典中的內(nèi)容呢?
閱讀推特的接口文檔你能看到接口的響應(yīng)示例搏恤。所得的NSDictionary
與這個(gè)結(jié)構(gòu)相似违寿,里面有個(gè)叫statuses
的鍵對(duì)應(yīng)值為裝載推特的NSArray
,推特?cái)?shù)據(jù)也是NSDictionary
類型挑社。
RWTweet
已經(jīng)包含一個(gè)類方法tweetWithStatus:
陨界,用以從給定格式的NSDictionary
中提取數(shù)據(jù)。所以你需要做的只是編寫一個(gè)循環(huán)痛阻,并遍歷整個(gè)數(shù)組,為每條推特創(chuàng)建一個(gè)RWTweet
的實(shí)例腮敌。
但是阱当,別這樣做。之后又更好的解決方法呢糜工。
這篇文章是關(guān)于ReactiveCocoa和函數(shù)式編程的弊添。數(shù)據(jù)轉(zhuǎn)換時(shí)使用函數(shù)式的接口會(huì)顯得更加干練。你可以使用LinqToObjectiveC來完成這個(gè)任務(wù)捌木。
關(guān)閉項(xiàng)目油坝,并打開你在第一個(gè)教程中使用TextEdit創(chuàng)建的Podfile
文件(譯注:這里作者混亂了,指的是本項(xiàng)目中的Podfile
文件刨裆,下載時(shí)已經(jīng)提供的澈圈,也并不是上一教程里創(chuàng)建的)。更新文件帆啃,添加新的依賴:
platform :ios, '7.0'
pod 'ReactiveCocoa', '2.1.8'
pod 'LinqToObjectiveC', '2.0.0'
打開終端并跳轉(zhuǎn)到此文件夾瞬女,執(zhí)行以下命令:
pod update
你會(huì)看到和以下相似的輸出:
Analyzing dependencies
Downloading dependencies
Installing LinqToObjectiveC (2.0.0)
Using ReactiveCocoa (2.1.8)
Generating Pods project
Integrating client project
重新打開workspace文件并確認(rèn)新的框架已經(jīng)如下圖一樣成功引入:
打開RWSearchFormViewController.m
并在文件頂端添加引用如下:
#import "RWTweet.h"
#import "NSArray+LinqExtensions.h"
NSArray+LinqExtensions.h
頭文件是LinqToObjectiveC
的一部分,這為NSArray
添加了很多方法努潘,用流式接口實(shí)現(xiàn)轉(zhuǎn)換诽偷,排序坤学,分組和過濾數(shù)據(jù)。
現(xiàn)在就立即使用這些API……更新在viewDidLoad
方法末端的管道如下:
[[[[[[self requestAccessToTwitterSignal]
then:^RACSignal *{
@strongify(self)
return self.searchText.rac_textSignal;
}]
filter:^BOOL(NSString *text) {
@strongify(self)
return [self isValidSearchText:text];
}]
flattenMap:^RACStream *(NSString *text) {
@strongify(self)
return [self signalForSearchWithText:text];
}]
deliverOn:[RACScheduler mainThreadScheduler]]
subscribeNext:^(NSDictionary *jsonSearchResult) {
NSArray *statuses = jsonSearchResult[@"statuses"];
NSArray *tweets = [statuses linq_select:^id(id tweet) {
return [RWTweet tweetWithStatus:tweet];
}];
[self.resultsViewController displayTweets:tweets];
} error:^(NSError *error) {
NSLog(@"An error occurred: %@", error);
}];
如你所見报慕,subscribeNext:
的block首先獲取了推特的NSArray
深浮。linq_select
方法將裝有NSDictionary
實(shí)例的數(shù)組通過提供的block處理轉(zhuǎn)換成新的數(shù)組元素,最后返回一個(gè)裝有RWTweet
實(shí)例的數(shù)組眠冈。
轉(zhuǎn)換成功后略号,相關(guān)推特就會(huì)被發(fā)送到結(jié)果視圖控制器。最后編譯運(yùn)行洋闽,你就能看到推特展示在UI界面中:
注意:ReactiveCocoa和LinqToObjectiveC有著相似的靈感來源玄柠。ReactiveCocoa是模仿微軟的Reactive Extensions框架,LinqToObjectiveC則是模仿它們的語言集成查詢接口(Language Integrated Query APIs)诫舅,或稱作LINQ羽利,特別是用于對(duì)象的LINQ。
異步加載圖片
你可能已經(jīng)發(fā)現(xiàn)在每一條推特的左邊有一塊間隙刊懈。那個(gè)位置是用來展示推特用戶的頭像的这弧。
RWTweet
類已經(jīng)有了一個(gè)profileImageUrl
屬性以記錄獲取這張圖片的URL。為了使列表平滑地滾動(dòng)虚汛,你需要確保從提供的URL中獲取圖片的代碼不在主線程中執(zhí)行匾浪。這可以使用Grand Central Dispatch(GCD)或者NSOperationQueue實(shí)現(xiàn)。但是為什么不直接使用ReactiveCocoa呢卷哩?
打開RWSearchResultsViewController.m
并在文件最后添加如下方法:
-(RACSignal *)signalForLoadingImage:(NSString *)imageUrl {
RACScheduler *scheduler = [RACScheduler
schedulerWithPriority:RACSchedulerPriorityBackground];
return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]];
UIImage *image = [UIImage imageWithData:data];
[subscriber sendNext:image];
[subscriber sendCompleted];
return nil;
}] subscribeOn:scheduler];
}
你現(xiàn)在應(yīng)該相當(dāng)熟悉這種模式了蛋辈!
由于你希望這個(gè)信號(hào)不在主線程中執(zhí)行,上面的方法先獲取了一個(gè)后臺(tái)調(diào)度器将谊。然后創(chuàng)建一個(gè)信號(hào)冷溶,該信號(hào)在有訂閱者時(shí)下載圖像數(shù)據(jù)并生成UIImage
。最后一步就是使用subscribeOn:
尊浓,以保證信號(hào)在提供的調(diào)度器中執(zhí)行逞频。
搞定!
現(xiàn)在栋齿,在同一個(gè)文件中更新tableView:cellForRowAtIndex:
方法苗胀,在方法返回前添加以下代碼:
cell.twitterAvatarView.image = nil;
[[[self signalForLoadingImage:tweet.profileImageUrl]
deliverOn:[RACScheduler mainThreadScheduler]]
subscribeNext:^(UIImage *image) {
cell.twitterAvatarView.image = image;
}];
在上面的代碼中,由于這些單元(cell)會(huì)重復(fù)使用并可能包含先前遺留的數(shù)據(jù)瓦堵,因而首先重置了圖片基协。然后創(chuàng)建用以獲取圖片數(shù)據(jù)的信號(hào)。而接下來你先前遇到過的deliverOn:
方法將next
事件調(diào)整到了主線程上谷丸,以便安全地執(zhí)行subscribeNext:
中的block堡掏。
多么簡(jiǎn)單有效!
編譯運(yùn)行后就能看到頭像現(xiàn)在都已經(jīng)正確顯示了:
節(jié)流
你可能已經(jīng)發(fā)現(xiàn)刨疼,每當(dāng)你輸入一個(gè)新的字符泉唁,就會(huì)立馬執(zhí)行一次新的推特搜索鹅龄。如果你是一個(gè)熟練的打字員(或者只是按緊刪格鍵),這會(huì)導(dǎo)致應(yīng)用在一秒內(nèi)作出多個(gè)搜索請(qǐng)求亭畜。這種實(shí)現(xiàn)并不理想扮休,原因有二:第一,這在對(duì)推特的搜索接口造成沖擊的同時(shí)舍棄了大部分返回的結(jié)果拴鸵;第二玷坠,不斷的更新結(jié)果會(huì)擾亂用戶的注意力!
更好的實(shí)現(xiàn)應(yīng)該是當(dāng)搜索文本在一個(gè)短時(shí)間內(nèi)劲藐,比如說500毫秒八堡,沒有改變的話再執(zhí)行搜索。正如你可能猜到的那樣聘芜,ReactiveCocoa還是很容易就能實(shí)現(xiàn)這一點(diǎn)兄渺!
打開RWSearchFormViewController.m
,更新viewDidLoad
末端的管道汰现,在過濾后新增節(jié)流操作:
[[[[[[[self requestAccessToTwitterSignal]
then:^RACSignal *{
@strongify(self)
return self.searchText.rac_textSignal;
}]
filter:^BOOL(NSString *text) {
@strongify(self)
return [self isValidSearchText:text];
}]
throttle:0.5]
flattenMap:^RACStream *(NSString *text) {
@strongify(self)
return [self signalForSearchWithText:text];
}]
deliverOn:[RACScheduler mainThreadScheduler]]
subscribeNext:^(NSDictionary *jsonSearchResult) {
NSArray *statuses = jsonSearchResult[@"statuses"];
NSArray *tweets = [statuses linq_select:^id(id tweet) {
return [RWTweet tweetWithStatus:tweet];
}];
[self.resultsViewController displayTweets:tweets];
} error:^(NSError *error) {
NSLog(@"An error occurred: %@", error);
}];
throttle
操作只有在時(shí)間間隔內(nèi)沒有接收到新的next
事件時(shí)才會(huì)發(fā)送next
事件給下一環(huán)節(jié)挂谍。這是不是相當(dāng)簡(jiǎn)單!
編譯運(yùn)行瞎饲,這時(shí)搜索結(jié)果只在停止輸入超過500毫秒時(shí)才會(huì)更新口叙。這感覺好多了對(duì)嗎?你的用戶也會(huì)這么想的嗅战。
并且……隨著最后一步的完成妄田,你的推特即時(shí)搜索應(yīng)用已經(jīng)完成了。給自己一點(diǎn)掌聲并跳支舞放松一下吧仗哨!
如果你在教程的過程中感到迷惑的話形庭,你可以下載瀏覽最終的項(xiàng)目(當(dāng)然別忘了在打開前在項(xiàng)目所在目錄運(yùn)行pod instal
命令),你也可以在GitHub找到這個(gè)項(xiàng)目厌漂,那里有對(duì)應(yīng)教程中每一步操作的提交記錄。
總結(jié)
在結(jié)束教程并給自己泡上一杯咖啡慶祝之前斟珊,非常值得欣賞一下項(xiàng)目最終搭建的管道苇倡。
這是一個(gè)相當(dāng)復(fù)雜的數(shù)據(jù)流,但所有都簡(jiǎn)明的表達(dá)在了一個(gè)響應(yīng)式管道中囤踩。這是多么迷人的景象爸冀贰!你可以想象如果使用非響應(yīng)式技術(shù)的話來時(shí)實(shí)現(xiàn)這些功能的話堵漱,應(yīng)用該變得多么復(fù)雜嗎综慎?而且要理清楚數(shù)據(jù)的流向?qū)⒆兊枚嗝蠢щy?聽著就覺得夠麻煩的了勤庐,而你現(xiàn)在已經(jīng)不再需要重蹈覆轍了示惊!
現(xiàn)在你體會(huì)ReactiveCocoa是多么了不起了吧好港!
最后一點(diǎn),ReactiveCocoa讓使用又稱為MVVM的Model View ViewModel設(shè)計(jì)模式變?yōu)榭赡苊追#涓行У姆蛛x了應(yīng)用邏輯和視圖邏輯钧汹。如果有人對(duì)后續(xù)關(guān)于用ReactiveCocoa實(shí)現(xiàn)MVVM的文章感興趣的話,請(qǐng)?jiān)谠u(píng)論中告訴我录择。我非常希望聽到能夠你的想法和經(jīng)驗(yàn)0卫场(譯注:后續(xù)也有作者關(guān)于MVVM的教程,有時(shí)間會(huì)繼續(xù)進(jìn)行翻譯0摺)