優雅的PromiseKit

背景

以前就瞭解到js中有Promise這麼一個東西,能夠很友好的實現異步方法,後來偶然在一段ios開源代碼中看到這麼一段用法:ios

firstly {
    login()
}.then { creds in
    fetch(avatar: creds.user)
}.done { image in
    self.imageView = image
}
複製代碼

眼前一亮,firstly第一步作xxx,then接下來作xxx,done完成了以後最後作xxx,這個寫法真是太swift了,頓時產生了興趣。 雖然實現異步回調我也有ReactCocoa的方案,但其中不乏一些晦澀難懂的知識須要理解,例如冷信號與熱信號,最讓人吐槽的仍是它的語法,寫一個簡單的邏輯就須要new各類Producer,切線程調用的方法又總是分不清subscribeOn和observeOn,並且放的位置不一樣還影響執行順序。 總之,在看到Promise語法以後,世界變得美好多了,接下來咱們就進入Promise的世界吧。git

PromiseKit

then & done

Promise對象就是一個ReactCocoa中的SignalProducer,它能夠異步fullfill返回一個成功對象或者reject返回一個錯誤信號。github

Promise { sink in
    it.requestJson().on(failed: { err in
        sink.reject(err)
    }, value: { data in
        sink.fulfill(data)
    }).start()
}
複製代碼

接下來就是把它用在各個方法塊裏面了,例如:json

firstly {
    Promise { sink in
        indicator.show(inView: view, text: text, detailText: nil, animated: true)
        sink.fulfill()
    }
}.then {
        api.promise(format: .json)
}.ensure {
        indicator.hide(inView: view, animated: true)
}.done { data in
        let params = data.result!["args"] as! [String: String]
        assert((Constant.baseParams + Constant.params) == params)
}.catch { error in
        assertionFailure()
}
        
複製代碼

firstly是可選的,它只能放在第一個,是爲了代碼能更加的優雅和整齊,他的block裏也是return一個Promise。 then是接在中間的,能夠無限多個then相互鏈接,顧名思義,就像咱們講故事能夠不斷地有而後、而後、而後...then也是要求返回一個Promise對象的,也就是說,任何一個then均可以拋出一個error,中斷事件。 ensure相似於finally,無論事件是否錯誤,它都必定會獲得執行,ensure不一樣於finally的是,它能夠放在任何位置。 done是事件結束的標誌,它是必需要有的,只有上面的事件都執行成功時,纔會最終執行done。 catch是捕獲異常,done以前的任何事件出現錯誤,都會直接進入catch。swift

上面代碼的含義就是先顯示loading,而後請求api,無論api是否請求成功,都要確保loading隱藏,而後若是成功,則打印數據,不然打印異常。api

Guarantee

Guarantee是Promise的特殊狀況,當咱們確保事件不會有錯誤的時候,就能夠用Guarantee來代替Promise,有它就不須要catch來捕獲異常了:promise

firstly {
    after(seconds: 0.1)
}.done {
    // there is no way to add a `catch` because after cannot fail.
}
複製代碼

after是一個延遲執行的方法,它就返回了一個Guarantee對象,由於延遲執行是必定不會失敗的,因此咱們只須要後續接done就好了。併發

map

map是指一次數據的變換,而不是一次事件,例如咱們要把從接口返回的json數據轉換成對象,就能夠用map,map返回的也是一個對象,而不是Promise。異步

tap

tap是一個無侵入的事件,相似於Reactivecocoa的doNext,他不會影響事件的任何屬性,只是在適當的時機作一些不影響主線的事情,適用於打點:ide

firstly {
    foo()
}.tap {
    print($0)
}.done {
    //…
}.catch {
    //…
}
複製代碼

when

when是個能夠並行執行多個任務的好東西,when中當全部事件都執行完成,或者有任何一個事件執行失敗,都會讓事件進入下一階段,when還有一個concurrently屬性,能夠控制併發執行任務的最多數量:

firstly {
    Promise { sink in
        indicator.show(inView: view, text: text, detailText: nil, animated: true)
        sink.fulfill()
    }
}.then {
        when(fulfilled: api.promise(format: .json), api2.promise(format: .json))
}.ensure {
        indicator.hide(inView: view, animated: true)
}.done { data, data2 in
        assertionFailure()
        expectation.fulfill()
}.catch { error in
        assert((error as! APError).description == err.description)
        expectation.fulfill()
}
複製代碼

這個方法仍是很經常使用的,當咱們要同時等2,3個接口的數據都拿到,再作後續的事情的時候,就適合用when了。

on

PromiseKit的切換線程很是的方便和直觀,只須要在方法中傳入on的線程便可:

firstly {
    user()
}.then(on: DispatchQueue.global()) { user in
    URLSession.shared.dataTask(.promise, with: user.imageUrl)
}.compactMap(on: DispatchQueue.global()) {
    UIImage(data: $0)
}
複製代碼

哪一個方法須要指定線程就在那個方法的on傳入對應的線程。

throw

若是then中須要拋出異常,一種方法是在Promise中調用reject,另外一種比較簡便的方法就是直接throw:

firstly {
    foo()
}.then { baz in
    bar(baz)
}.then { result in
    guard !result.isBad else { throw MyError.myIssue }
    //…
    return doOtherThing()
}
複製代碼

若是調用的方法可能會拋出異常,try也會讓異常直達catch:

foo().then { baz in
    bar(baz)
}.then { result in
    try doOtherThing()
}.catch { error in
    // if doOtherThing() throws, we end up here
}
複製代碼

recover

CLLocationManager.requestLocation().recover { error -> Promise<CLLocation> in
    guard error == MyError.airplaneMode else {
        throw error
    }
    return .value(CLLocation.savannah)
}.done { location in
    //…
}
複製代碼

recover能從異常中拯救任務,能夠斷定某些錯誤就忽略,當作正常結果返回,剩下的錯誤繼續拋出異常。

幾個例子

列表每行順序依次漸變消失

let fade = Guarantee()
for cell in tableView.visibleCells {
    fade = fade.then {
        UIView.animate(.promise, duration: 0.1) {
            cell.alpha = 0
        }
    }
}
fade.done {
    // finish
}
複製代碼

執行一個方法,指定超時時間

let fetches: [Promise<T>] = makeFetches()
let timeout = after(seconds: 4)

race(when(fulfilled: fetches).asVoid(), timeout).then {
    //…
}

複製代碼

race和when不同,when會等待全部任務執行成功再繼續,race是誰第一個到就繼續,race要求全部任務返回類型必須同樣,最好的作法是都返回Void,上面的例子就是讓4秒計時和請求api同時發起,若是4秒計時到了請求還沒回來,則直接調用後續方法。

至少等待一段時間作某件事

let waitAtLeast = after(seconds: 0.3)

firstly {
    foo()
}.then {
    waitAtLeast
}.done {
    //…
}
複製代碼

上面的例子從firstly中的foo執行以前就已經開始after(seconds: 0.3),因此若是foo執行超過0.3秒,則foo執行完後不會再等待0.3秒,而是直接繼續下一個任務。若是foo執行不到0.3秒,則會等待到0.3秒再繼續。這個方法的場景能夠用在啓動頁動畫,動畫顯示須要一個保證時間。

重試

func attempt<T>(maximumRetryCount: Int = 3, delayBeforeRetry: DispatchTimeInterval = .seconds(2), _ body: @escaping () -> Promise<T>) -> Promise<T> {
    var attempts = 0
    func attempt() -> Promise<T> {
        attempts += 1
        return body().recover { error -> Promise<T> in
            guard attempts < maximumRetryCount else { throw error }
            return after(delayBeforeRetry).then(on: nil, attempt)
        }
    }
    return attempt()
}

attempt(maximumRetryCount: 3) {
    flakeyTask(parameters: foo)
}.then {
    //…
}.catch { _ in
    // we attempted three times but still failed
}
複製代碼

Delegate變Promise

extension CLLocationManager {
    static func promise() -> Promise<CLLocation> {
        return PMKCLLocationManagerProxy().promise
    }
}

class PMKCLLocationManagerProxy: NSObject, CLLocationManagerDelegate {
    private let (promise, seal) = Promise<[CLLocation]>.pending()
    private var retainCycle: PMKCLLocationManagerProxy?
    private let manager = CLLocationManager()

    init() {
        super.init()
        retainCycle = self
        manager.delegate = self // does not retain hence the `retainCycle` property

        promise.ensure {
            // ensure we break the retain cycle
            self.retainCycle = nil
        }
    }

    @objc fileprivate func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        seal.fulfill(locations)
    }

    @objc func locationManager(_: CLLocationManager, didFailWithError error: Error) {
        seal.reject(error)
    }
}

// use:

CLLocationManager.promise().then { locations in
    //…
}.catch { error in
    //…
}
複製代碼

retainCycle是其中一個循環引用,目的是爲了避免讓PMKCLLocationManagerProxy自身被釋放,當Promise結束的時候,在ensure方法中執行self.retainCycle = nil把引用解除,來達到釋放自身的目的,很是巧妙。

傳遞中間結果

有時候咱們須要傳遞任務中的一些中間結果,好比下面的例子,done中沒法使用username變量:

login().then { username in
    fetch(avatar: username)
}.done { image in
    //…
}
複製代碼

能夠經過map巧妙的把結果變成元組形式返回:

login().then { username in
    fetch(avatar: username).map { ($0, username) }
}.then { image, username in
    //…
}
複製代碼

總結

儘管PromiseKit不少用法和原理都和Reactivecocoa類似,但它語法的簡潔和直觀是它最大的特色,光是這一點就足夠吸引你們去喜歡它了~

相關文章
相關標籤/搜索