項(xiàng)目準(zhǔn)備工作
我們的App會(huì)在Github上搜索特定名稱的項(xiàng)目,在UITextField里輸入項(xiàng)目名稱,我們就自動(dòng)在Github上搜索項(xiàng)目的名字,并在下面的UITableView中顯示一些項(xiàng)目信息顯示出來。

然后,在ViewController里,添加對(duì)應(yīng)的IBOutlet:
@IBOutlet weak var repositoryName: UITextField!
@IBOutlet weak var searchResult: UITableView!
以及DisposeBag:
var bag: DisposeBag! = DisposeBag()
最后,通過CocoaPods安裝項(xiàng)目需要的組件:
# Uncomment this line to define a global platform for your project
platform :ios, '9.0'
# Uncomment this line if you're using Swift
use_frameworks!
target 'RxNetworkDemo' do
pod 'Alamofire', '~> 3.4'
pod 'RxSwift', '~> 2.0'
pod 'RxCocoa', '~> 2.0'
pod 'SwiftyJSON', :git => 'https://github.com/SwiftyJSON/SwiftyJSON.git'
end
并且,在ViewController里,引入對(duì)應(yīng)的組件:
import UIKit
import RxSwift
import RxCocoa
import Alamofire
import SwiftyJSON
這樣,我們就做好所有的準(zhǔn)備工作了。
控制網(wǎng)絡(luò)請(qǐng)求頻度
發(fā)送請(qǐng)求之前,我們要先通過UITextField獲取用戶輸入。很簡(jiǎn)單,直接訂閱UITextField的rx_text就可以了,在viewDidLoad方法里,添加下面的代碼:
self.repositoryName.rx_text
.subscribeNext {
print("Search item: \($0)")
}.addDisposableTo(self.bag)
執(zhí)行后,會(huì)發(fā)現(xiàn),控制臺(tái)里的結(jié)果是這樣的:

第一次的空白字符串是UI加載的時(shí)候,監(jiān)聽到的事件值;第二次空白是UITextField獲取輸入事件的時(shí)候間聽到的事件值;而后,我們每輸入一個(gè)字符,就會(huì)監(jiān)聽到一個(gè)不同的事件。
如果我們用這樣的結(jié)果來作為在Github上搜索的內(nèi)容,會(huì)有一些問題:
- 我們用空的字符串進(jìn)行了搜索,明顯是錯(cuò)誤的;
- 當(dāng)輸入只有1,2個(gè)字符時(shí),發(fā)起的搜索明顯是不精準(zhǔn)的;
- 當(dāng)輸入的名稱較長時(shí),輸入過程會(huì)發(fā)起大量無效的搜索(例如:僅僅是輸入RxSwift,就發(fā)起了9次);
首先,我們來解決前兩個(gè)問題。
使用filter過濾事件內(nèi)容
我們要先過濾掉過短的輸入,例如,當(dāng)用戶輸入2個(gè)以上的字符時(shí)才進(jìn)行查詢。很簡(jiǎn)單,在訂閱前,使用filter(n)對(duì)事件值進(jìn)行過濾就可以了:
self.repositoryName.rx_text
.filter {
return $0.characters.count > 2
}
.subscribeNext {
print("search item: \($0)")
}.addDisposableTo(self.bag)
.filter的參數(shù)是事件值的類型,在我們的例子里,也就是String,返回一個(gè)Bool,表示是否要向訂閱者發(fā)送事件。
重新運(yùn)行,就會(huì)發(fā)現(xiàn)我們過濾掉了一些明顯無效的輸入:

盡管如此,我們還是訂閱到了5次事件,如果每次訂閱到都發(fā)起請(qǐng)求,還是太頻繁了,我們希望進(jìn)一步控制請(qǐng)求的頻率。
使用throttle控制請(qǐng)求頻度
我們可以使用throttle在指定的時(shí)間間隔里,忽略掉發(fā)生的事件。這樣,就不會(huì)每次輸入都訂閱到事件了。繼續(xù)修改訂閱代碼:
self.repositoryName.rx_text
.filter {
return $0.characters.count > 2
}
.throttle(0.5, scheduler: MainScheduler.instance)
.subscribeNext {
print("search item: \($0)")
}.addDisposableTo(self.bag)
throttle的第一個(gè)參數(shù)表示希望忽略的時(shí)間間隔,第二個(gè)參數(shù)表示在主線程中運(yùn)行計(jì)時(shí)器。
重新運(yùn)行,這次,控制臺(tái)里的結(jié)果基本就可用了:

接下來的思路就很簡(jiǎn)單了,我們直接在訂閱到的事件里,調(diào)用Github API查詢項(xiàng)目,并把查詢結(jié)果更新到TableView里就好了。
思路雖然簡(jiǎn)單,卻關(guān)聯(lián)到了不少的實(shí)現(xiàn)細(xì)節(jié),我們先來完成網(wǎng)絡(luò)請(qǐng)求的部分。
包裝Alamofire成Observable
我們先給ViewController添加一個(gè)extension,所有和網(wǎng)絡(luò)相關(guān)的代碼,都放到這個(gè)extension里:
extension ViewController {
}
我們希望最終訂閱到的事件值,是個(gè)包含我們需要內(nèi)容的Key-Value集合,簡(jiǎn)單起見,我們添加一個(gè)類型的別名:
typealias RepositoryInfo = Dictionary<String, AnyObject>
在這個(gè)Dictionary中,String用于索引結(jié)果中的內(nèi)容,而值有可能是整數(shù)、有可能是字符串,因此我們定義成了AnyObject。
接下來,我們?cè)?code>ViewController extension中,添加一個(gè)方法:
private func searchForGithub(repositoryName: String)
-> Observable<RepositoryInfo>
searchForGithub接受一個(gè)表示,表示要查詢的repository的名字,返回一個(gè)事件值類型是RepositoryInfo的事件序列。
怎么實(shí)現(xiàn)呢?
之前的視頻里我們也提到過,RxSwift提供了一個(gè)叫做create的方法,可以讓我們自定義事件序列。對(duì)于封裝一個(gè)網(wǎng)絡(luò)請(qǐng)求來說,它簡(jiǎn)直再合適不過了。
在searchForGithub里,添加下面的代碼:
private func searchForGithub(repositoryName: String)
-> Observable<RepositoryInfo> {
return Observable.create {
(observer: AnyObserver<RepositoryInfo>) -> Disposable in
let url = "https://api.github.com/search/repositories"
let parameters = [
"q": repositoryName + " stars:>=2000"
]
let request = Alamofire.request(.GET, url,
parameters: parameters,
encoding: .URLEncodedInURL)
}
}
create接受一個(gè)closure參數(shù),這個(gè)closure參數(shù)本質(zhì)上和我們之前用過的subscribeNext方法是類似的。它接受一個(gè)AnyObserver,并返回一個(gè)Disposable對(duì)象。
在這里,AnyObserver表示要?jiǎng)?chuàng)建的事件序列的訂閱者。稍候,我們要根據(jù)請(qǐng)求的不同結(jié)果,向這個(gè)訂閱者發(fā)送事件。由于我們要返回的Observable的事件值類型是RepositoryInfo,因此,這里AnyObserver可以訂閱到的事件值的類型,也是RepositoryInfo。
然后,在這個(gè)Clousre的實(shí)現(xiàn)里,我們先分別添加了請(qǐng)求的URL,以及附帶的參數(shù)。其中:
- 參數(shù)q表示要查詢的項(xiàng)目名;
- “ start:>=2000”是Github的項(xiàng)目查詢語言,表示查詢大于2000星的項(xiàng)目;
最后,直接用Alamofire.request請(qǐng)求了Github API。為了先了解下這個(gè)API的返回值,我們不妨先在瀏覽器里看一下調(diào)用結(jié)果:
https://api.github.com/search/repositories?q=RxSwift%20stars:>=2000

在返回的JSON里,大致分成幾大部分:
- total_count:表示查詢到的repository個(gè)數(shù);
- incomplete_results:表示是否返回的是部分結(jié)果;
- items是一個(gè)JSON對(duì)象數(shù)組,包含了每一個(gè)查詢到的repository的詳細(xì)信息;
稍候,我們就會(huì)接收這個(gè)結(jié)果集,把他篩選成下面這樣:
{
"total_count": 1,
"items": [
{
"full_name": "RxSwift",
"description": "ReactiveX/RxSwift"
"html_url": "Reactive Programming in Swift",
"avatar_url": "https://avatars.githubusercontent.com/u/6407041?v=3"
}
]
}
然后,返回給事件的訂閱者。至此,這一切都還不太難理解。接下來,重頭戲就來了。
自定義向訂閱者發(fā)送的事件
接下來我們要進(jìn)行的工作,是使用create方法自定義Observable的重點(diǎn),我們需要根據(jù)Github的返回值,來定義向訂閱者返回的內(nèi)容。
把Alamofire.request部分的代碼,添加上結(jié)果處理:
let request = Alamofire.request(.GET, url,
parameters: parameters,
encoding: .URLEncodedInURL)
.responseJSON { response in
switch response.result {
case .Success(let json):
// How can we handle success event?
case .Failure(let error):
observer.on(.Error(error))
}
}
當(dāng)請(qǐng)求失敗的時(shí)候,我們的處理邏輯很簡(jiǎn)單:
- 直接把返回的
NSError對(duì)象封裝在Event.Error里; - 通過
on方法把事件發(fā)送給訂閱者;
那成功的時(shí)候呢?發(fā)送事件的部分,當(dāng)然也是如法炮制,用on方法就好了,我們發(fā)送些什么呢?
- 我們首先要把返回的結(jié)果做一些篩選,只找出我們需要使用的數(shù)據(jù);
- 當(dāng)請(qǐng)求成功時(shí),我們要先發(fā)送.Next事件,傳遞事件值,然后發(fā)送.Completed事件,表示結(jié)束;
使用SwiftyJSON過濾返回結(jié)果
首先來實(shí)現(xiàn)第一步,對(duì)返回結(jié)果進(jìn)行篩選,在ViewController extension中,添加一個(gè)新的方法:
private func parseGithubResponse(
response: AnyObject) -> RepositoryInfo
它接受一個(gè)AnyObject作為參數(shù),我們會(huì)傳遞請(qǐng)求成功時(shí).Success的associated value,然后返回要發(fā)送給訂閱者的RepositoryInfo。
在parseGithubResponse的實(shí)現(xiàn)里,我們使用SwiftyJSON來簡(jiǎn)化JSON串的處理:
private func parseGithubResponse(
response: AnyObject) -> RepositoryInfo {
let json = JSON(response);
let totalCount = json["total_count"].int!
var ret: RepositoryInfo = [
"total_count": totalCount,
"items": []
];
}
在上面的代碼里:
-
JSON(response)用于初始化SwiftyJSON,我們可以得到一個(gè)Swity.JSON對(duì)象json; - 然后,就可以像訪問普通
Dictionary一樣去訪問JSON串中的內(nèi)容了,例如:json["total_count"]。如果我們確信它是個(gè)整數(shù),就直接訪問它的int屬性,讀取optional的值就可以了; - 我們構(gòu)建了一個(gè)最基本的返回值
ret,初始化了total_count;
查詢到了repository的個(gè)數(shù)之后,我們來處理返回結(jié)果中的“items”部分,它是一個(gè)JSON數(shù)組,數(shù)組中的每一個(gè)對(duì)象,都表示一個(gè)repository。同樣,SwiftyJSON也有方便我們處理數(shù)組的方法。在ret的定義后面,繼續(xù)添加下面的代碼:
if totalCount != 0 {
let items = json["items"]
var info: [RepositoryInfo] = []
for (_, subJson):(String, JSON) in items {
let fullName = subJson["full_name"].string!
let description = subJson["description"].string!
let htmlUrl = subJson["html_url"].string!
let avatarUrl = subJson["owner"]["avatar_url"].string!
info.append([
"full_name": fullName,
"description": description,
"html_url": htmlUrl,
"avatar_url": avatarUrl
])
}
ret["items"] = info
}
在上面的代碼里,當(dāng)查詢到的repository不為0時(shí):
首先,我們使用json["items"]讀取到了JSON數(shù)組,它仍舊是一個(gè)Swity.JSON對(duì)象;
其次,我們定義了一個(gè)存儲(chǔ)items信息的RepositoryInfo數(shù)組,用于保存篩選過的內(nèi)容;
第三,盡管items是一個(gè)Swifty.JSON對(duì)象,我們?nèi)耘f可以使用for...in循環(huán)來遍歷它。對(duì)于items中的每一個(gè)key-value,我們可以把它理解為是一個(gè)(String, JSON)類型的Tuple,于是,我們用這樣的代碼:
let fullName = subJson["full_name"].string!
let description = subJson["description"].string!
let htmlUrl = subJson["html_url"].string!
let avatarUrl = subJson["owner"]["avatar_url"].string!
分別讀取了每一個(gè)項(xiàng)目的名稱、描述、網(wǎng)址以及創(chuàng)始人頭像。值得說明的是,當(dāng)讀取創(chuàng)始人頭像時(shí),由于owner索引的內(nèi)容又是一個(gè)JSON對(duì)象,因此,我們可以使用串聯(lián)索引的方式把嵌套的JSON串中的內(nèi)容讀取出來,很方便。
篩選出了所有需要的信息之后,我們就把內(nèi)容添加到用于保存篩選結(jié)果的數(shù)組里。最后全部篩選結(jié)束之后,我們就把info更新到返回值的“items”字段里。
最后,別忘記讓parseGithubResponse返回ret:
return ret
這樣我們就完成對(duì)結(jié)果的篩選了,最終我們得到了一個(gè)只包含我們感興趣的RepositoryInfo對(duì)象。這時(shí),我們回到主戰(zhàn)場(chǎng),處理Alamofire請(qǐng)求成功時(shí)的事件處理。
封裝.Next()事件
在之前.Success的case里,添加下面的代碼:
let request = Alamofire.request(.GET, url,
parameters: parameters,
encoding: .URLEncodedInURL)
.responseJSON { response in
switch response.result {
case .Success(let json):
// How can we handle success event?
let info = self.parseGithubResponse(json)
observer.on(.Next(info))
observer.on(.Completed)
case .Failure(let error):
observer.on(.Error(error))
}
}
其實(shí)很簡(jiǎn)單,我們只要把parseGithubResponse的返回值,直接作為.Next的associated value就可以了。這里,再次提醒大家,不要忘記在.Next之后發(fā)送.Completed。
處理Observable.create參數(shù)的返回值
至此,我們已經(jīng)完成了90%的工作,但是,現(xiàn)在還不是休息的時(shí)候。如果你記不清了,可以翻回頭看看Observable.create的參數(shù)定義,它接受的closure參數(shù)還要返回一個(gè)Disposable對(duì)象呢。這個(gè)對(duì)象,用于對(duì)create返回的Observable進(jìn)行“善后工作”。
在處理網(wǎng)絡(luò)請(qǐng)求的時(shí)候,無論因?yàn)槿魏卧颍?code>create創(chuàng)建的事件序列被銷毀了,那么我們最好取消掉正在執(zhí)行的網(wǎng)絡(luò)請(qǐng)求。因此,我們要添加一個(gè)AnonymousDisposable對(duì)象,他唯一的工作,就是取消網(wǎng)絡(luò)請(qǐng)求。在create的Closure方法最后,添加下面的代碼:
return AnonymousDisposable {
request.cancel()
}
如果request已經(jīng)完成了,調(diào)用cancel()也不會(huì)帶來任何問題。
如果我們創(chuàng)建的事件序列在被銷毀時(shí)無需執(zhí)行任何額外操作,我們也可以直接使用return NopDisposable返回一個(gè)“什么也不需要做的Disposable對(duì)象”。
這樣,使用create封裝網(wǎng)絡(luò)請(qǐng)求的功能就全部完成了。我們把每一次網(wǎng)絡(luò)請(qǐng)求,都封裝成了一個(gè)可以被訂閱的事件序列。
接下來,我們實(shí)現(xiàn)在UITextField中輸入后,自動(dòng)查詢的功能。別急,看似簡(jiǎn)單的事情,仍舊有新要點(diǎn)要注意。
使用.flatMap轉(zhuǎn)化Observable
基本思路是很簡(jiǎn)單的,把要發(fā)送給訂閱者的每一次UITextField輸入事件,在map()里調(diào)用searchForGithub方法,變成Github的查詢結(jié)果就好了。按照想象的在訂閱前添加下面的代碼:
self.repositoryName.rx_text
.filter {
return $0.characters.count > 2
}
.throttle(0.5, scheduler: MainScheduler.instance)
.map {
self.searchForGithub($0)
}
它可以正常工作,但是,執(zhí)行的方式一定和我們想象中有點(diǎn)兒差別。我們希望subscribeNext可以訂閱到一個(gè)事件值是RepositoryInfo的事件。
但是,由于self.searchForGithub($0)返回的是一個(gè)Observable<RepositoryInfo>,因此,我們訂閱到的實(shí)際上是一個(gè)事件序列。我們還需要在.subscribeNext里繼續(xù)訂閱它,這顯然不是我們想要的。
為了解決這樣的問題,RxSwift提供了另外一個(gè)映射事件序列的方法.flatMap,在它的實(shí)現(xiàn)里,我們可以找到這樣的注釋:
Projects each element of an observable sequence to an observable sequence and merges the resulting observable sequences into one observable sequence.
簡(jiǎn)單來說,就是如果經(jīng)過映射后的結(jié)果是一個(gè)新的事件序列,那么flatMap把映射前的事件(在我們的例子里是UITextField的輸入)和映射后的事件(在我們的例子里是一個(gè)網(wǎng)絡(luò)請(qǐng)求)合并成一個(gè)事件發(fā)送給訂閱者。
這樣,我們就可以直接在subscribeNext中訂閱到RepositoryInfo了。我們可以把各種訂閱到的值,輸出到控制臺(tái)上。
self.repositoryName.rx_text
.filter {
return $0.characters.count > 2
}
.throttle(0.5, scheduler: MainScheduler.instance)
.flatMap {
self.searchForGithub($0)
}
.subscribeNext {
let repoCount = $0["total_count"] as! Int;
let repoItems = $0["items"] as! [RepositoryInfo];
if repoCount != 0 {
print("item count: \(repoCount)")
for item in repoItems {
print("---------------------------------")
let name = item["full_name"]
let description = item["description"]
let avatarUrl = item["avatar_url"]
print("full name: \(name)")
print("description: \(description)")
print("avatar_url: \(avatarUrl)")
}
}
}.addDisposableTo(self.bag)
這樣,我們就實(shí)現(xiàn)實(shí)時(shí)響應(yīng)查詢的功能了,編譯執(zhí)行,我們就可以在控制臺(tái)看到結(jié)果了:
