原文:ReactiveCocoa Tutorial – The Definitive Introduction: Part 2/2
ReactiveCocoa 是一個(gè)允許你在 iOS 應(yīng)用中使用函數(shù)響應(yīng)式編程(FRP)技術(shù)的框架。通過(guò) ReactiveCocoa 系列教程的第一部分,你學(xué)會(huì)了如何用發(fā)出事件流的信號(hào)來(lái)替換標(biāo)準(zhǔn)的 action 動(dòng)作和事件處理邏輯。你還學(xué)習(xí)了如何轉(zhuǎn)換、分割和組合這些信號(hào)。
在這個(gè)系列的第二部分中,你將學(xué)習(xí) ReactiveCocoa 更高級(jí)的功能。包括:
- 另外兩個(gè)事件類(lèi)型:error 和 completed
- Throttling(節(jié)流?)
- Threading(多線(xiàn)程)
- Continuations(延續(xù)?)
- ... 還有更多
是時(shí)候深入挖掘了!
Twitter 實(shí)例
在整個(gè)教程中,你要開(kāi)發(fā)的應(yīng)用程序叫做 Twitter Instant(仿照 Google Instant 的概念),這是一個(gè) Twitter 搜索應(yīng)用程序,可以在你輸入時(shí)實(shí)時(shí)更新搜索結(jié)果。
這個(gè)應(yīng)用程序的初始化項(xiàng)目包括基本的用戶(hù)界面和一些你需要開(kāi)始使用的普通代碼。與第1部分一樣,你需要使用 CocoaPods 來(lái)獲取ReactiveCocoa 框架并將其集成到您的項(xiàng)目中。啟動(dòng)項(xiàng)目已經(jīng)包含了必要的 Podfile,所以打開(kāi)終端窗口并執(zhí)行以下命令:
pod install
如果執(zhí)行正確,你應(yīng)該看到類(lèi)似于下面的輸出。
Analyzing dependencies
Downloading dependencies
Using ReactiveCocoa (2.1.8)
Generating Pods project
Integrating client project
這應(yīng)該已經(jīng)生成了一個(gè) Xcode 工作空間,TwitterInstant.xcworkspace。在 Xcode 中打開(kāi)它,確認(rèn)它包含兩個(gè)項(xiàng)目。
- TwitterInstant:這是你的應(yīng)用程序邏輯所在的地方。
- Pods: 這是外部依賴(lài)的地方。目前它只包含 ReactiveCocoa。
編譯并運(yùn)行。下面的界面會(huì)迎接你:
花點(diǎn)時(shí)間熟悉一下應(yīng)用程序的代碼。這是一個(gè)非常簡(jiǎn)單的基于 split view controller 的應(yīng)用。左側(cè)面板是 RWSearchFormViewController,它通過(guò)故事板添加了一些 UI 控件,搜索文本輸入框連接到一個(gè)插座。右側(cè)面板是 RWSearchResultsViewController,它目前只是一個(gè) UITableViewController 子類(lèi)。
如果你打開(kāi) RWSearchFormViewController.m,你可以看到 viewDidLoad
方法中獲取了結(jié)果視圖控制器,并將其賦值給私有屬性 resultsViewController
。你的大部分應(yīng)用邏輯將體現(xiàn)在 RWSearchFormViewController 中,這個(gè)屬性將向 RWSearchResultsViewController 提供搜索結(jié)果。
驗(yàn)證搜索文本
你要做的第一件事是驗(yàn)證搜索文本,以確保它的長(zhǎng)度大于兩個(gè)字符。如果你完成了本系列的第一部分,這應(yīng)該是一個(gè)愉快的復(fù)習(xí):
- (BOOL)isValidSearchText:(NSString *)text {
return text.length > 2;
}
這只是確保輸入的搜索字符串長(zhǎng)度大于兩個(gè)字符。有了這樣簡(jiǎn)單的邏輯,你可能會(huì)問(wèn) "為什么要在項(xiàng)目文件中單獨(dú)設(shè)置一個(gè)方法?"
目前的邏輯很簡(jiǎn)單。但如果將來(lái)需要更復(fù)雜的邏輯呢?通過(guò)上面的封裝,你只會(huì)在一個(gè)地方進(jìn)行修改。此外,上面的實(shí)現(xiàn)可以讓你的代碼更有表現(xiàn)力,它表明了你為什么要檢查字符串的長(zhǎng)度。我們都要遵循良好的編碼實(shí)踐,對(duì)嗎?
在同一個(gè)文件的頂部,導(dǎo)入 ReactiveObjC:
#import <ReactiveObjC/ReactiveObjC.h>
在同一個(gè)文件中,在 viewDidLoad
的末尾添加以下內(nèi)容:
[[self.searchText.rac_textSignal
map:^id _Nullable (NSString *_Nullable value) {
return [self isValidSearchText:value] ? UIColor.whiteColor : UIColor.yellowColor;
}] subscribeNext:^(UIColor *color) {
self.searchText.backgroundColor = color;
}];
上面的代碼具體的內(nèi)容:
- 獲取搜索文本輸入框字段的文本信號(hào)。
- 將其轉(zhuǎn)換為背景色,表示輸入內(nèi)容是否有效。
- 然后在
subscribeNext:
塊中將信號(hào)傳遞過(guò)來(lái)的 UIColor 屬性應(yīng)用于輸入框的backgroundColor
屬性。
編譯并運(yùn)行,觀察當(dāng)搜索輸入框輸入的字符串太短時(shí),輸入框是如何用黃色背景來(lái)表示輸入內(nèi)容無(wú)效的。
用流程圖來(lái)說(shuō)明,這個(gè)簡(jiǎn)單的響應(yīng)式管道看起來(lái)有點(diǎn)像這樣:
每次輸入內(nèi)容發(fā)生變化時(shí),rac_textSignal
信號(hào)都會(huì)觸發(fā)包含當(dāng)前文本字段文本的 next
事件。map
步驟將文本值轉(zhuǎn)化為顏色,而 subscribeNext:
步驟則將這個(gè)值獲取并應(yīng)用到文本字段的背景上。
當(dāng)然,你還記得第一篇文章中的內(nèi)容吧?如果不記得的話(huà),你可能要在這里停下來(lái),至少讀一遍以練習(xí)。
在添加 Twitter 搜索邏輯之前,還有幾個(gè)有趣的話(huà)題要講。
管道格式化
當(dāng)你深入研究 ReactiveCocoa 代碼的格式化時(shí),普遍接受的慣例是讓每個(gè)操作都在新的行上,并將所有的步驟垂直對(duì)齊。
在下一張圖中,你可以看到一個(gè)更復(fù)雜的例子的對(duì)齊方式,這個(gè)例子取自于之前的教程:
這可以讓你非常容易地看到組成管道的操作。另外,盡量減少每個(gè) Block 塊中的代碼量;任何超過(guò)幾行的代碼都應(yīng)該被分解成一個(gè)私有方法。
不幸的是,Xcode 并不喜歡這種格式化的風(fēng)格,所以你可能會(huì)發(fā)現(xiàn)你自己在和它的自動(dòng)縮進(jìn)邏輯作斗爭(zhēng)!
內(nèi)存管理
考慮你添加到 TwitterInstant 應(yīng)用中的代碼,你是否想知道你剛剛創(chuàng)建的管道是如何保留的?當(dāng)然,由于它沒(méi)有被分配到變量或?qū)傩灾校运囊糜?jì)數(shù)不會(huì)遞增,注定要被銷(xiāo)毀?
ReactiveCocoa 的設(shè)計(jì)目標(biāo)之一就是允許這種編程風(fēng)格,管道可以匿名形成。在你到目前為止所寫(xiě)的所有響應(yīng)式代碼中,這看起來(lái)應(yīng)該是很直觀的。
為了支持這種模式,ReactiveCocoa 維護(hù)并保留了自己的全局信號(hào)集。如果它有一個(gè)或多個(gè)訂閱者,那么這個(gè)信號(hào)是活躍的。如果所有的訂閱者都被移除,那么信號(hào)就會(huì)被取消分配。關(guān)于 ReactiveCocoa 如何管理這個(gè)過(guò)程的更多信息,請(qǐng)參見(jiàn)內(nèi)存管理文檔。
最后一個(gè)問(wèn)題: 如何取消訂閱一個(gè)信號(hào)?在 completed
或 error
事件后,訂閱會(huì)自動(dòng)移除(您將很快了解更多信息)。你也可以通過(guò) RACDisposable
來(lái)實(shí)現(xiàn)手動(dòng)刪除信號(hào)。
RACSignal
上的訂閱方法都會(huì)返回一個(gè) RACDisposable
的實(shí)例,允許你通過(guò) dispose
方法手動(dòng)刪除訂閱。下面是一個(gè)使用當(dāng)前管道的快速示例:
// 返回包含 UIColor 實(shí)例的信號(hào)
RACSignal *backgroundColorSignal = [self.searchText.rac_textSignal map:^id _Nullable(NSString * _Nullable value) {
return [self isValidSearchText:value] ? UIColor.whiteColor : UIColor.yellowColor;
}];
// RACSignal 的訂閱方法會(huì)返回一個(gè) RACDisposable 的實(shí)例
RACDisposable *subscription = [backgroundColorSignal subscribeNext:^(UIColor *color) {
self.searchText.backgroundColor = color;
}];
// 在未來(lái)的某個(gè)時(shí)刻,手動(dòng)刪除信號(hào)的訂閱
[subscription dispose];
你會(huì)發(fā)現(xiàn)自己不可能經(jīng)常這樣做,但值得知道這種可能性的存在。
注意:作為一個(gè)必然的結(jié)果,如果你創(chuàng)建了一個(gè)管道(信號(hào))但沒(méi)有訂閱它,管道永遠(yuǎn)不會(huì)執(zhí)行,也包括任何
side-effects
方法,如doNext:
塊。
避免引循環(huán)
雖然 ReactiveCocoa 在幕后做了很多聰明的事情——這意味著你不用太擔(dān)心信號(hào)的內(nèi)存管理,但有一個(gè)重要的與內(nèi)存相關(guān)問(wèn)題你確實(shí)需要考慮。
如果你查看你剛剛添加的響應(yīng)式代碼:
[[self.searchText.rac_textSignal
map:^id _Nullable (NSString *_Nullable value) {
return [self isValidSearchText:value] ? UIColor.whiteColor : UIColor.yellowColor;
}] subscribeNext:^(UIColor *color) {
self.searchText.backgroundColor = color;
}];
subscribeNext: 塊使用 self
來(lái)獲取對(duì)文本輸入框的引用。Block 會(huì)捕獲并保留閉包中的值,因此,如果 self
和這個(gè)信號(hào)之間存在強(qiáng)引用,將導(dǎo)致引用循環(huán)問(wèn)題。這是否重要取決于 self
對(duì)象的生命周期。如果它的生命周期是應(yīng)用程序的持續(xù)時(shí)間,就像這里的情況一樣,這并不重要。但在更復(fù)雜的應(yīng)用中,這種情況很少。
為了避免這種潛在的引用循環(huán)問(wèn)題,Apple 的 Working With Blocks 文檔建議捕獲一個(gè)弱引用到 self
。在當(dāng)前的代碼中,你可以通過(guò)以下方式實(shí)現(xiàn):
__weak RWSearchFormViewController *weakSelf = self; // 捕獲弱引用
[[self.searchText.rac_textSignal
map:^id _Nullable (NSString *_Nullable value) {
return [self isValidSearchText:value] ? UIColor.whiteColor : UIColor.yellowColor;
}] subscribeNext:^(UIColor *color) {
weakSelf.searchText.backgroundColor = color;
}];
以上代碼中,weakSelf
是對(duì) self
的引用,為了使它成為一個(gè)弱引用,它被標(biāo)記為 __weak
。請(qǐng)注意,subscribeNext:
塊現(xiàn)在使用了 weakSelf
變量。這看起來(lái)不是很優(yōu)雅!
ReactiveCocoa 框架中包含了一個(gè)小技巧,你可以用它來(lái)代替上面的代碼。在文件的頂部添加以下導(dǎo)入:
#import "RACEXTScope.h"
然后用下面的代碼代替上面的代碼:
@weakify(self)
[[self.searchText.rac_textSignal
map:^id _Nullable (NSString *_Nullable value) {
return [self isValidSearchText:value] ? UIColor.whiteColor : UIColor.yellowColor;
}] subscribeNext:^(UIColor *color) {
@strongify(self)
self.searchText.backgroundColor = color;
}];
上面的 @weakify
和 @strongify
語(yǔ)句是在 Extended Objective-C 庫(kù)中定義的宏,它們也包含在 ReactiveCocoa 中。@weakify
宏允許你創(chuàng)建影子變量(shadow variables),也就是弱引用(如果你需要多個(gè)弱引用,可以傳遞多個(gè)變量),@strongify
宏允許你對(duì)之前傳遞給 @weakify
的變量創(chuàng)建強(qiáng)引用。
注意:如果你有興趣了解
@weakify
和@strongify
的實(shí)際作用,在 Xcode 中選擇 Product -> Perform Action -> Preprocess "RWSearchForViewController"。這將對(duì)視圖控制器進(jìn)行預(yù)處理,展開(kāi)所有的宏,并允許你看到最終的輸出。
最后需要注意的是,在 Block 塊中使用實(shí)例變量時(shí)要小心。這些也會(huì)導(dǎo)致 Block 塊捕捉到對(duì) self
的強(qiáng)引用。如果是你的代碼導(dǎo)致這個(gè)問(wèn)題,你可以打開(kāi)編譯器警告來(lái)提醒你。在項(xiàng)目的構(gòu)建設(shè)置中搜索 retain,可以找到下面的選項(xiàng):
好了,你從理論知識(shí)中幸存下來(lái)了,恭喜你!現(xiàn)在你聰明多了,準(zhǔn)備好進(jìn)入有趣的部分:為你的應(yīng)用程序添加一些真正的功能。
注:關(guān)注過(guò)上一篇教程的敏銳讀者無(wú)疑會(huì)注意到,通過(guò)利用
RAC
宏,你可以在當(dāng)前管道中取消對(duì)subscribeNext:
塊的調(diào)用。如果你發(fā)現(xiàn)了這一點(diǎn),請(qǐng)做出這樣的改變,并為自己頒發(fā)一顆閃亮的金星!
// 通過(guò) RAC 宏實(shí)現(xiàn):將信號(hào)的輸出分配給 self.searchText 對(duì)象的 backgroundColor 屬性
RAC(self.searchText, backgroundColor) = [self.searchText.rac_textSignal map:^id _Nullable(NSString * _Nullable value) {
return [self isValidSearchText:value] ? UIColor.whiteColor : UIColor.yellowColor;
}];
請(qǐng)求訪(fǎng)問(wèn) Twitter
你將使用 Social Framework 來(lái)允許 TwitterInstant 應(yīng)用程序搜索 Tweets,并使用 Accounts Framework 來(lái)授予對(duì) Twitter 的訪(fǎng)問(wèn)權(quán)。要想更詳細(xì)地了解 Social Framework,請(qǐng)查看 iOS 6 教程 中專(zhuān)門(mén)為這個(gè)框架編寫(xiě)的章節(jié)。
在添加代碼之前,你需要將你的 Twitter 憑證輸入到模擬器或你正在運(yùn)行這款應(yīng)用的 iPad 上。打開(kāi) Settings 應(yīng)用,選擇 Twitter 菜單選項(xiàng),然后在屏幕右側(cè)添加你的憑證:
初始化項(xiàng)目已經(jīng)添加了所需的框架,所以你只需要導(dǎo)入頭文件。在 RWSearchFormViewController.m 文件中,在文件的頂部添加以下代碼:
#import <Accounts/Accounts.h>
#import <Social/Social.h>
在 import
語(yǔ)句的下面添加以下枚舉和常量:
typedef NS_ENUM(NSUInteger, RWTwitterInstantError) {
RWTwitterInstantErrorAccessDenied,
RWTwitterInstantErrorNoTwitterAccounts,
RWTwitterInstantErrorInvalidResponse,
};
static NSString *const RWTwitterInstantDomain = @"TwitterInstant";
你將很快使用它們來(lái)識(shí)別錯(cuò)誤。在同一個(gè)文件的下面,在現(xiàn)有屬性聲明的下面,添加以下內(nèi)容:
@property (strong, nonatomic) ACAccountStore *accountStore;
@property (strong, nonatomic) ACAccountType *twitterAccountType;
ACAccountsStore
類(lèi)提供了通過(guò)設(shè)備連接到各種社交媒體賬戶(hù)的訪(fǎng)問(wèn),而 ACAccountType
類(lèi)則代表了一種特定的賬戶(hù)類(lèi)型。
在同一個(gè)文件中再往下,在 viewDidLoad
的末尾添加以下內(nèi)容:
self.accountStore = [[ACAccountStore alloc] init];
// Deprecated,該功能似乎已經(jīng)失效!頭文件中建議使用 Twitter SDK 實(shí)現(xiàn)
self.twitterAccountType = [self.accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];
這將創(chuàng)建賬戶(hù)管理器(account store)和 Twitter 賬戶(hù)標(biāo)識(shí)符。
當(dāng)一個(gè)應(yīng)用程序請(qǐng)求訪(fǎng)問(wèn)一個(gè)社交媒體賬戶(hù)時(shí),用戶(hù)會(huì)看到一個(gè)彈出窗口。這是一個(gè)異步操作,因此它是一個(gè)很好的信號(hào)包裝的候選者,以便響應(yīng)式地使用它。
在同一個(gè)文件中,再往下添加以下方法:
- (RACSignal *)requestAccessToTwitterSignal {
// 1. 定義一個(gè)錯(cuò)誤
NSError *accessError = [NSError errorWithDomain:RWTwitterInstantDomain code:RWTwitterInstantErrorAccessDenied userInfo:nil];
// 2. 創(chuàng)建一個(gè)信號(hào)
@weakify(self)
return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
// 3. 請(qǐng)求訪(fǎng)問(wèn) Twitter
@strongify(self)
[self.accountStore requestAccessToAccountsWithType:self.twitterAccountType options:nil completion:^(BOOL granted, NSError *error) {
// 4. 處理請(qǐng)求響應(yīng)
if (!granted) {
[subscriber sendError:accessError];
} else {
[subscriber sendNext:nil];
[subscriber sendCompleted];
}
}];
return nil;
}];
}
這個(gè)方法的作用如下:
- 定義了一個(gè)錯(cuò)誤,如果用戶(hù)拒絕訪(fǎng)問(wèn),就會(huì)發(fā)送錯(cuò)誤。
- 參考教程一,類(lèi)方法
createSignal
返回了一個(gè)RACSignal
的實(shí)例。 - 通過(guò)賬戶(hù)管理器請(qǐng)求訪(fǎng)問(wèn) Twitter。此時(shí),用戶(hù)會(huì)看到一個(gè)提示,要求他們授予這個(gè)應(yīng)用對(duì)其 Twitter 賬戶(hù)的訪(fǎng)問(wèn)權(quán)。
- 在用戶(hù)授予或拒絕訪(fǎng)問(wèn)后,就會(huì)發(fā)出信號(hào)事件。如果用戶(hù)授予訪(fǎng)問(wèn)權(quán),則會(huì)發(fā)出 next 事件,然后是 completed 事件。如果用戶(hù)拒絕訪(fǎng)問(wèn),則會(huì)發(fā)出 error 事件。
如果你還記得我們的第一篇教程,一個(gè)信號(hào)可以發(fā)出三種不同的事件類(lèi)型:
- Next
- Completed
- Error
在一個(gè)信號(hào)的生命周期中,它可能不發(fā)出任何事件,一個(gè)或多個(gè) next 事件,然后緊接著是一個(gè) completed 事件或一個(gè) error 事件。
最后,為了利用這個(gè)信號(hào),在 viewDidLoad
方法的結(jié)尾添加以下內(nèi)容:
[self requestAccessToTwitterSignal] subscribeNext:^(id _Nullable x) {
NSLog(@"Access granted");
} error:^(NSError * _Nullable error) {
NSLog(@"An error occurred: %@", error);
};
如果你編譯并運(yùn)行應(yīng)用,應(yīng)該會(huì)有以下提示:
如果你點(diǎn)擊 OK,則應(yīng)在控制臺(tái)中出現(xiàn) subscribeNext:
塊中的日志信息,而如果你點(diǎn)擊 Don't Allow,則會(huì)執(zhí)行 error Block 塊并記錄相應(yīng)的信息。
賬戶(hù)框架會(huì)記住你所做的決定。因此,如果要測(cè)試這兩種不同的選擇,你需要通過(guò) iOS 模擬器->重置內(nèi)容和設(shè)置......菜單選項(xiàng)來(lái)重置模擬器。這個(gè)過(guò)程有點(diǎn)痛苦,因?yàn)槟氵€必須重新輸入你的 Twitter 賬戶(hù)憑證!
鏈接信號(hào)
一旦用戶(hù)同意了(希望如此)應(yīng)用對(duì)其 Twitter 賬戶(hù)的訪(fǎng)問(wèn)權(quán)限,應(yīng)用程序就需要持續(xù)監(jiān)控搜索文本字段的變化,以便查詢(xún) twitter。
應(yīng)用程序需要等待請(qǐng)求訪(fǎng)問(wèn)用戶(hù) Twitter 的信號(hào)發(fā)出 completed 事件,然后訂閱文本字段的信號(hào)。不同信號(hào)的順序鏈?zhǔn)且粋€(gè)常見(jiàn)的問(wèn)題,但 ReactiveCocoa 處理得非常優(yōu)雅。
在 viewDidLoad
的結(jié)尾處用以下內(nèi)容替換你當(dāng)前的管道:
[[[self requestAccessToTwitterSignal]
then:^RACSignal * _Nonnull{
@strongify(self)
return self.searchText.rac_textSignal;
}] subscribeNext:^(id _Nullable x) {
NSLog(@"Access granted");
} error:^(NSError * _Nullable error) {
NSLog(@"An error occurred: %@", error);
}];
then
方法等到一個(gè) completed 事件發(fā)出后,再訂閱其 Block 塊參數(shù)重返回的信號(hào)。這就有效地將控制權(quán)從一個(gè)信號(hào)傳遞到下一個(gè)信號(hào)。
注意:你已經(jīng)在為位于這個(gè)管道上方的管道中弱化了
self
,所以沒(méi)有必要在這個(gè)管道之前使用@weakify(self)
。
then
方法也會(huì)將 error 事件傳遞過(guò)去。因此,最后的 subscribeNext:error:
塊仍然會(huì)接收初始訪(fǎng)問(wèn)請(qǐng)求步驟發(fā)出的錯(cuò)誤。
當(dāng)你編譯并運(yùn)行應(yīng)用,然后授予訪(fǎng)問(wèn)權(quán)時(shí),你應(yīng)該看到你輸入到搜索字段的文本記錄在控制臺(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!
接下來(lái),在管道中添加一個(gè) filter
過(guò)濾操作,以刪除任何無(wú)效的搜索字符串。在本例中,它們是由少于三個(gè)字符組成的字符串:
[[[[self requestAccessToTwitterSignal]
then:^RACSignal * _Nonnull{
@strongify(self)
return self.searchText.rac_textSignal;
}] filter:^BOOL(id _Nullable value) {
@strongify(self)
return [self isValidSearchText:value];
}] subscribeNext:^(id _Nullable x) {
NSLog(@"Access granted");
} error:^(NSError * _Nullable error) {
NSLog(@"An error occurred: %@", error);
}];
編譯并再次運(yùn)行,觀察過(guò)濾的運(yùn)行情況:
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!
用圖形化的方式來(lái)說(shuō)明當(dāng)前的應(yīng)用管道,它是這樣的:
應(yīng)用流水線(xiàn)從 requestAccessToTwitterSignal
開(kāi)始,然后切換到 rac_textSignal
。同時(shí),next 事件通過(guò)過(guò)濾器,最后進(jìn)入訂閱Block 塊。你還可以看到第一步發(fā)出的任何錯(cuò)誤事件都會(huì)被同一個(gè) subscribeNext:error:
塊所消耗。
現(xiàn)在,你已經(jīng)有了一個(gè)發(fā)出搜索文本的信號(hào),是時(shí)候用它來(lái)搜索 Twitter了! 你玩得開(kāi)心嗎?你應(yīng)該是的,因?yàn)楝F(xiàn)在你真的有收獲了。
搜索 Twitter
Social Framework 是訪(fǎng)問(wèn) Twitter 搜索 API 的一個(gè)選項(xiàng)。然而,正如你所預(yù)料的那樣,Social Framework 并不是響應(yīng)式的! 下一步是將所需的 API 方法調(diào)用包裹在一個(gè)信號(hào)中。你現(xiàn)在應(yīng)該已經(jīng)掌握了這個(gè)過(guò)程的竅門(mén)了!
在 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;
}
這將創(chuàng)建一個(gè)通過(guò) v1.1 REST API 搜索 Twitter 的請(qǐng)求。上面的代碼使用 q
搜索參數(shù)來(lái)搜索包含給定搜索字符串的推文。你可以在 T witter API 文檔中閱讀更多關(guān)于這個(gè)搜索API,以及其他你可以傳遞的參數(shù)。
下一步是根據(jù)這個(gè)請(qǐng)求創(chuàng)建一個(gè)信號(hào)。在同一個(gè)文件中,添加以下方法:
- (RACSignal *)signalForSearchWithText:(NSString *)text {
// 1. 定義錯(cuò)誤
NSError *noAccountsError = [NSError errorWithDomain:RWTwitterInstantDomain code:RWTwitterInstantErrorNoTwitterAccounts userInfo:nil];
NSError *invalidResponseError = [NSError errorWithDomain:RWTwitterInstantDomain code:RWTwitterInstantErrorInvalidResponse userInfo:nil];
// 2. 創(chuàng)建信號(hào) block
@weakify(self)
return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
@strongify(self)
// 3.創(chuàng)建請(qǐng)求
SLRequest *request = [self requestforTwitterSearchWithText:text];
// 4.請(qǐng)求 twitter 賬戶(hù)
NSArray *twitterAccounts = [self.accountStore accountsWithAccountType:self.twitterAccountType];
if (twitterAccounts.count == 0) {
[subscriber sendError:noAccountsError];
} else {
[request setAccount:twitterAccounts.lastObject];
// 5. 執(zhí)行請(qǐng)求
[request performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
if (urlResponse.statusCode == 200) {
// 6. 一旦請(qǐng)求成功,解析響應(yīng)
NSDictionary *timelineData = [NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingAllowFragments error:nil];
[subscriber sendNext:timelineData];
[subscriber sendCompleted];
} else {
// 7. 發(fā)送請(qǐng)求失敗的錯(cuò)誤信號(hào)
[subscriber sendError:invalidResponseError];
}
}];
}
return nil;
}];
}
依次進(jìn)行每一步:
- 起初,你需要定義幾個(gè)不同的錯(cuò)誤,一個(gè)表示用戶(hù)沒(méi)有在他們的設(shè)備上添加任何 Twitter 賬戶(hù), 另一個(gè)表示在執(zhí)行查詢(xún)時(shí)出錯(cuò)。
- 和之前一樣,創(chuàng)建一個(gè)信號(hào)。
- 使用你在上一步添加的方法為給定的搜索字符串創(chuàng)建一個(gè)請(qǐng)求。
- 查詢(xún)賬戶(hù)商店,找到第一個(gè)可用的 Twitter 賬戶(hù)。如果沒(méi)有給定賬戶(hù),則會(huì)發(fā)出一個(gè)錯(cuò)誤。
- 執(zhí)行該請(qǐng)求。
- 在響應(yīng)成功的情況下(HTTP 響應(yīng)狀態(tài)碼 200),返回的 JSON 數(shù)據(jù)將被解析并作為 next 事件一起發(fā)出,隨后是一個(gè) completed 事件。
- 如果是不成功的響應(yīng),則會(huì)發(fā)出一個(gè) error 事件。
現(xiàn)在要把這個(gè)新信號(hào)用起來(lái)了!
在本教程的第一部分,你學(xué)習(xí)了如何使用 flattenMap
將每個(gè) next 事件 map 轉(zhuǎn)移到一個(gè)新的信號(hào)上,然后再訂閱。現(xiàn)在是時(shí)候再次使用這個(gè)信號(hào)了。在 viewDidLoad
的結(jié)尾處,通過(guò)在結(jié)尾處添加一個(gè) flattenMap
步驟來(lái)更新你的應(yīng)用管道:
[[[[[self requestAccessToTwitterSignal]
then:^RACSignal * _Nonnull{
@strongify(self)
return self.searchText.rac_textSignal;
}] filter:^BOOL(id _Nullable value) {
@strongify(self)
return [self isValidSearchText:value];
}] flattenMap:^__kindof RACSignal * _Nullable(id _Nullable value) {
@strongify(self)
return [self signalForSearchWithText:value];
}] subscribeNext:^(id _Nullable x) {
NSLog(@"Access granted");
} error:^(NSError * _Nullable error) {
NSLog(@"An error occurred: %@", error);
}];
編譯并運(yùn)行,然后在搜索文本字段中輸入一些文本。一旦文本至少有三個(gè)字符或更多的長(zhǎng)度,你應(yīng)該在控制臺(tái)窗口中看到 Twitter 搜索的結(jié)果。
下面顯示的只是你將看到的數(shù)據(jù)種類(lèi)的一個(gè)片段:
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:
方法也會(huì)發(fā)出錯(cuò)誤事件,subscribeNext:error:
塊會(huì)消耗這些錯(cuò)誤事件。你可以相信我的話(huà),但你可能想測(cè)試一下!
在模擬器內(nèi)打開(kāi) Settings 應(yīng)用,選擇你的 Twitter 賬戶(hù),然后點(diǎn)擊 Delete Account 按鈕刪除它。
如果你重新運(yùn)行應(yīng)用程序,它仍然被允許訪(fǎng)問(wèn)用戶(hù)的 Twitter 賬戶(hù),但沒(méi)有賬戶(hù)可用。因此,signalForSearchText
方法會(huì)發(fā)出一個(gè)錯(cuò)誤,并被記錄下來(lá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
表示這是 RWTwitterInstantErrorNoTwitterAccounts
錯(cuò)誤。在生產(chǎn)應(yīng)用中,你會(huì)希望打開(kāi)錯(cuò)誤代碼,做一些更有意義的事情,而不僅僅是記錄結(jié)果。
這說(shuō)明了關(guān)于 error 事件的一個(gè)重要觀點(diǎn),只要一個(gè)信號(hào)發(fā)出錯(cuò)誤,它就會(huì)直接跳到錯(cuò)誤處理 Block 塊。這是一個(gè)特殊的流程。
注:當(dāng) Twitter 請(qǐng)求返回錯(cuò)誤時(shí),可以去行使另一個(gè)特殊流程。這里有一個(gè)快速提示,嘗試將請(qǐng)求參數(shù)改為無(wú)效的東西!
線(xiàn)程
我相信你一定心癢難耐,想把 Twitter 搜索返回的 JSON 數(shù)據(jù)輸出并顯示到 UI 中,但在這之前,你還需要做最后一件事。要知道這是什么,你需要做一些探索!
在下面指定的位置為 subscribeNext:error:
步驟添加一個(gè)斷點(diǎn):
重新運(yùn)行應(yīng)用程序,如果需要的話(huà),再次重新輸入你的 Twitter 證書(shū),然后在搜索欄中輸入一些文本。當(dāng)斷點(diǎn)到達(dá)時(shí),你應(yīng)該看到類(lèi)似下圖的東西:
請(qǐng)注意,調(diào)試器打出中斷的代碼并沒(méi)有在主線(xiàn)程上執(zhí)行,主線(xiàn)程在上面的截圖中顯示為 Thread 1。請(qǐng)記住,最重要的是你只從主線(xiàn)程更新 UI,因此如果你想在 UI 中顯示推文列表,你就必須切換線(xiàn)程。
這說(shuō)明了關(guān)于 ReactiveCocoa 框架的一個(gè)重要觀點(diǎn)。上圖所示的操作會(huì)在信號(hào)最初發(fā)出事件的線(xiàn)程上執(zhí)行。試著在其他管道步驟處添加斷點(diǎn),你可能會(huì)驚訝地發(fā)現(xiàn)它們?cè)诙鄠€(gè)不同的線(xiàn)程上執(zhí)行!
那么你如何去更新 UI 呢?典型的方法是使用操作隊(duì)列(詳見(jiàn)本站其他地方的教程如何使用 NSOperations 和 NSOperationQueues),然而 ReactiveCocoa 有一個(gè)更簡(jiǎn)單的解決方案來(lái)解決這個(gè)問(wèn)題。
更新你的管道,在 flattenMap:
之后添加一個(gè) deliveryOn:
操作,如下所示:
[[[[[[self requestAccessToTwitterSignal]
then:^RACSignal * _Nonnull{
@strongify(self)
return self.searchText.rac_textSignal;
}] filter:^BOOL(id _Nullable value) {
@strongify(self)
return [self isValidSearchText:value];
}] flattenMap:^__kindof RACSignal * _Nullable(id _Nullable value) {
@strongify(self)
return [self signalForSearchWithText:value];
}] deliverOn:[RACScheduler mainThreadScheduler]]
subscribeNext:^(id _Nullable x) {
NSLog(@"Access granted");
} error:^(NSError * _Nullable error) {
NSLog(@"An error occurred: %@", error);
}];
現(xiàn)在重新運(yùn)行應(yīng)用程序,并輸入一些文本,使您的應(yīng)用程序擊中斷點(diǎn)。你應(yīng)該看到你的 subscribeNext:error:
塊中的日志語(yǔ)句現(xiàn)在正在主線(xiàn)程上執(zhí)行:
啥玩意兒?只需一個(gè)簡(jiǎn)單的操作,就能把事件的流向匯集到不同的線(xiàn)程上?這有多厲害!?
你可以放心地繼續(xù)更新你的 UI 了!
注意:如果你看一下
RACScheduler
類(lèi),你會(huì)發(fā)現(xiàn)有相當(dāng)多的選項(xiàng)可以在不同優(yōu)先級(jí)的線(xiàn)程上交付,或者在管道中添加延遲。
是時(shí)候看看那些推特了!
更新 UI
如果你打開(kāi) RWSearchResultsViewController.h
,你會(huì)發(fā)現(xiàn)它已經(jīng)有一個(gè) displayTweets:
方法,它將使右側(cè)視圖控制器渲染提供的推文數(shù)組。實(shí)現(xiàn)非常簡(jiǎn)單,它只是一個(gè)標(biāo)準(zhǔn)的 UITableView
數(shù)據(jù)源。displayTweets:
方法的單一參數(shù)期望一個(gè)包含 RWTweet
實(shí)例的 NSArray
。你還會(huì)發(fā)現(xiàn) RWTweet
模型對(duì)象是作為啟動(dòng)項(xiàng)目的一部分提供的。
目前到達(dá) subscibeNext:error:
步驟的數(shù)據(jù)是一個(gè) NSDictionary
,它是通過(guò)在 signalForSearchWithText:
方法中解析 JSON 響應(yīng)而構(gòu)建的。那么如何確定這個(gè)字典的內(nèi)容呢?
如果你看一下 Twitter API 文檔,你可以看到一個(gè)樣本響應(yīng)。NSDictionary
鏡像了這個(gè)結(jié)構(gòu),所以你應(yīng)該會(huì)發(fā)現(xiàn)它有一個(gè)名為 status
的鍵,它是一個(gè) NSArray
的 tweets
,也是 NSDictionary
實(shí)例。
如果你看一下 RWTweet
,它已經(jīng)有一個(gè)類(lèi)方法 tweetWithStatus:
它接收一個(gè)給定格式的 NSDictionary
,并提取所需數(shù)據(jù)。所以,你需要做的就是寫(xiě)一個(gè) for
循環(huán),然后在數(shù)組中迭代,為每一條 tweet
創(chuàng)建一個(gè) RWTweet
實(shí)例。
然而,你不會(huì)這么做的! 哦,不,還有更好的東西在等著你呢!
本文介紹的是 ReactiveCocoa 和函數(shù)式編程。當(dāng)你使用函數(shù)式 API 時(shí),將數(shù)據(jù)從一種格式轉(zhuǎn)換為另一種格式會(huì)更加優(yōu)雅。你將使用LinqToObjectiveC 來(lái)執(zhí)行這個(gè)任務(wù)。
關(guān)閉 TwitterInstant 工作區(qū),然后在 TextEdit 中打開(kāi)在第一個(gè)教程中創(chuàng)建的 Podfile。更新文件以添加新的依賴(lài)關(guān)系:
# 指明依賴(lài)庫(kù)的來(lái)源地址,不使用默認(rèn) CDN
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'
target 'TwitterInstant' do
pod 'ReactiveObjC', '~> 3.1.1'
pod 'LinqToObjectiveC', '~> 2.1.0'
end
在同一文件夾中打開(kāi)終端窗口,并發(fā)出以下命令:
pod update
您將看到類(lèi)似于以下內(nèi)容的輸出:
Analyzing dependencies
Downloading dependencies
Installing LinqToObjectiveC (2.1.0)
Generating Pods project
Integrating client project
Pod installation complete! There are 2 dependencies from the Podfile and 2 total pods installed.
重新打開(kāi) Xcode workspace,驗(yàn)證新的 pod 是否顯示,如下圖所示:
打開(kāi) RWSearchFormViewController.m
,并在文件頂部添加以下 import 代碼:
#import "RWTweet.h"
#import <NSArray+LinqExtensions.h>
NSArray+LinqExtensions.h
頭文件來(lái)自 LinqToObjectiveC
,它為 NSArray
添加了許多方法,允許你使用流暢的 API 對(duì)其數(shù)據(jù)進(jìn)行轉(zhuǎn)換、排序、分組和過(guò)濾。
現(xiàn)在要把這個(gè) API 用起來(lái)......在 viewDidLoad
結(jié)尾處更新當(dāng)前管道,如下所示:
[[[[[[self requestAccessToTwitterSignal]
then:^RACSignal * _Nonnull{
@strongify(self)
return self.searchText.rac_textSignal;
}] filter:^BOOL(id _Nullable value) {
@strongify(self)
return [self isValidSearchText:value];
}] flattenMap:^__kindof RACSignal * _Nullable(id _Nullable value) {
@strongify(self)
return [self signalForSearchWithText:value];
}] 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 * _Nullable error) {
NSLog(@"An error occurred: %@", error);
}];
如上所示,subscribeNext:
塊首先獲取推文的 NSArray
。linq_select
方法通過(guò)在每個(gè)數(shù)組元素上執(zhí)行所提供的 Block 塊來(lái)轉(zhuǎn)換NSDictionary
實(shí)例數(shù)組,從而得到一個(gè) RWTweet
實(shí)例數(shù)組。
一旦轉(zhuǎn)換完畢,推文就會(huì)被發(fā)送到結(jié)果視圖控制器。
構(gòu)建并運(yùn)行,最終可以看到推文出現(xiàn)在 UI 中。
注:ReactiveCocoa 和 LinqToObjectiveC 的靈感來(lái)源相似。ReactiveCocoa 是以微軟的 Reactive Extensions 庫(kù)為藍(lán)本,而 LinqToObjectiveC 則是以他們的語(yǔ)言集成查詢(xún) API,或者 LINQ,特別是 Linq to Objects 為藍(lán)本。
異步加載圖片
你可能已經(jīng)注意到了,每條推特的左邊都有一個(gè)空隙。這個(gè)空間是用來(lái)顯示 Twitter 用戶(hù)的頭像的。
RWTweet
類(lèi)已經(jīng)有一個(gè) profileImageUrl
屬性,該屬性被填充了一個(gè)合適的 URL
來(lái)獲取這個(gè)圖片。為了使表格視圖能夠順利滾動(dòng),你需要確保從給定的 URL
中獲取這張圖片的代碼不在主線(xiàn)程上執(zhí)行。這可以使用 Grand Central Dispatch 或 NSOperationQueue 來(lái)實(shí)現(xiàn)。但為什么不使用 ReactiveCocoa 呢?
打開(kāi) RWSearchResultsViewController.m,在文件末尾添加以下方法:
// 異步加載圖片
- (RACSignal *)signalForLoadingImage:(NSString *)imageUrl {
RACScheduler *scheduler = [RACScheduler schedulerWithPriority:RACSchedulerPriorityBackground];
return [[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull 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)該對(duì)這個(gè)模式很熟悉了吧!
上面的方法首先獲得一個(gè)后臺(tái)調(diào)度器,因?yàn)槟阆M@個(gè)信號(hào)在主線(xiàn)程以外的線(xiàn)程上執(zhí)行。接下來(lái),它創(chuàng)建了一個(gè)下載圖像數(shù)據(jù)的信號(hào),并在有訂閱者時(shí)創(chuàng)建一個(gè) UIImage
。最后一個(gè)魔法是 subscribeOn:
,它確保信號(hào)在給定的調(diào)度器上執(zhí)行。
神奇!
現(xiàn)在,在同一個(gè)文件中,更新 tableView:cellForRowAtIndex:
方法,在 return
語(yǔ)句前添加以下內(nèi)容:
cell.twitterAvatarView.image = nil;
[[[self signalForLoadingImage:tweet.profileImageUrl]
deliverOn:[RACScheduler mainThreadScheduler]]
subscribeNext:^(UIImage *image) {
cell.twitterAvatarView.image = image;
}];
首先首先重置圖像,因?yàn)檫@些單元格是重復(fù)使用的,因此可能包含陳舊的數(shù)據(jù)。然后創(chuàng)建所需的信號(hào)來(lái)獲取圖像數(shù)據(jù)。你之前遇到的 deliverOn:
管道步驟,將 next 事件調(diào)度到主線(xiàn)程上執(zhí)行,這樣 subscribeNext:
塊就可以安全執(zhí)行了。
很好,很簡(jiǎn)單!
構(gòu)建并運(yùn)行,看看頭像現(xiàn)在是否能正確顯示:
節(jié)流(Throtting)
你可能已經(jīng)注意到,每當(dāng)你輸入一個(gè)新的字符,Twitter 搜索就會(huì)立即執(zhí)行。如果你是一個(gè)快速打字的人(或者干脆按住刪除鍵),這可能會(huì)導(dǎo)致應(yīng)用程序一秒鐘執(zhí)行幾次搜索。這并不理想,原因有以下幾點(diǎn):首先,你在敲打 Twitter 搜索API,同時(shí)也丟掉了大部分搜索結(jié)果。第二,你在不斷地更新結(jié)果,這對(duì)用戶(hù)來(lái)說(shuō)是相當(dāng)分心的。
一個(gè)更好的方法是,只有在搜索文本在短時(shí)間內(nèi)沒(méi)有變化的情況下才執(zhí)行搜索,比如 500 毫秒。
你可能已經(jīng)猜到了,ReactiveCocoa 讓這個(gè)任務(wù)變得非常簡(jiǎn)單!
打開(kāi) RWSearchFormViewController.m,在 viewDidLoad
結(jié)尾處更新管道,在 filter
過(guò)濾器之后添加一個(gè) throttle
步驟:
[[[[[[[self requestAccessToTwitterSignal]
then:^RACSignal * _Nonnull{
@strongify(self)
return self.searchText.rac_textSignal;
}] filter:^BOOL(id _Nullable value) {
@strongify(self)
return [self isValidSearchText:value];
}] throttle:0.5]
flattenMap:^__kindof RACSignal * _Nullable(id _Nullable value) {
@strongify(self)
return [self signalForSearchWithText:value];
}] 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 * _Nullable error) {
NSLog(@"An error occurred: %@", error);
}];
只有在給定的時(shí)間段內(nèi)沒(méi)有收到另一個(gè)下一個(gè)事件時(shí),節(jié)流操作才會(huì)發(fā)送下一個(gè)事件。真的就這么簡(jiǎn)單!
構(gòu)建并運(yùn)行確認(rèn),只有當(dāng)你停止輸入時(shí)長(zhǎng)超過(guò) 500 毫秒時(shí),搜索結(jié)果才會(huì)更新。感覺(jué)好多了不是嗎?你的用戶(hù)也會(huì)這么認(rèn)為。
而且......有了最后一步,你的 Twitter 即時(shí)應(yīng)用就完成了。給自己拍拍背,跳個(gè)快樂(lè)的舞蹈吧。
如果你在教程中的某個(gè)地方迷失了,你可以下載最終項(xiàng)目(別忘了在打開(kāi)之前從項(xiàng)目的目錄中運(yùn)行 pod install
),或者你可以從GitHub上獲取代碼,在 GitHub 上,本教程中的每個(gè)構(gòu)建和運(yùn)行步驟都有一個(gè)提交。
總結(jié)
在出發(fā)享受一杯勝利咖啡之前,有必要欣賞一下最后的應(yīng)用程序流水線(xiàn):
這是相當(dāng)復(fù)雜的數(shù)據(jù)流,都簡(jiǎn)明扼要地表達(dá)為一條響應(yīng)式管道。這真是一道美麗的風(fēng)景線(xiàn)! 你能想象,如果使用非響應(yīng)式技術(shù),這個(gè)應(yīng)用會(huì)有多復(fù)雜嗎?而要看到這樣的應(yīng)用中的數(shù)據(jù)流又會(huì)有多難呢?聽(tīng)起來(lái)非常繁瑣,現(xiàn)在你再也不用走這條路了!
現(xiàn)在你知道 ReactiveCocoa 真的相當(dāng)厲害了吧!
最后一點(diǎn),ReactiveCocoa 使得使用 Model View ViewModel,也就是 MVVM 設(shè)計(jì)模式成為可能,它可以更好地分離應(yīng)用邏輯和視圖邏輯。如果有人對(duì)關(guān)于 MVVM 與 ReactiveCocoa 的后續(xù)文章感興趣,請(qǐng)?jiān)谠u(píng)論中告訴我。我很想聽(tīng)聽(tīng)你的想法和經(jīng)驗(yàn)!