基于RxSwift的網(wǎng)絡(luò)編程 - I

項(xiàng)目準(zhǔn)備工作

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

image

然后,在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)單,直接訂閱UITextFieldrx_text就可以了,在viewDidLoad方法里,添加下面的代碼:

self.repositoryName.rx_text
    .subscribeNext {
        print("Search item: \($0)")
    }.addDisposableTo(self.bag)

執(zhí)行后,會(huì)發(fā)現(xiàn),控制臺(tái)里的結(jié)果是這樣的:

image

第一次的空白字符串是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)我們過濾掉了一些明顯無效的輸入:

image

盡管如此,我們還是訂閱到了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é)果基本就可用了:

image

接下來的思路就很簡(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

image

在返回的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)單:

  1. 直接把返回的NSError對(duì)象封裝在Event.Error里;
  2. 通過on方法把事件發(fā)送給訂閱者;

那成功的時(shí)候呢?發(fā)送事件的部分,當(dāng)然也是如法炮制,用on方法就好了,我們發(fā)送些什么呢?

  1. 我們首先要把返回的結(jié)果做一些篩選,只找出我們需要使用的數(shù)據(jù);
  2. 當(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": []
    ];
}

在上面的代碼里:

  1. JSON(response)用于初始化SwiftyJSON,我們可以得到一個(gè)Swity.JSON對(duì)象json
  2. 然后,就可以像訪問普通Dictionary一樣去訪問JSON串中的內(nèi)容了,例如:json["total_count"]。如果我們確信它是個(gè)整數(shù),就直接訪問它的int屬性,讀取optional的值就可以了;
  3. 我們構(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é)果了:

image

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容