優雅的PromiseKit
背景
之前就瞭解到js中有Promise這麼一個東西,可以很友好的實現非同步方法,後來偶然在一段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的世界吧。
ofollow,noindex">PromiseKit
then & done
Promise物件就是一個ReactCocoa中的SignalProducer,它可以非同步fullfill返回一個成功物件或者reject返回一個錯誤訊號。
Promise { sink in it.requestJson().on(failed: { err in sink.reject(err) }, value: { data in sink.fulfill(data) }).start() } 複製程式碼
接下來就是把它用在各個方法塊裡面了,例如:
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。
上面程式碼的含義就是先顯示loading,然後請求api,不管api是否請求成功,都要確保loading隱藏,然後如果成功,則列印資料,否則列印異常。
Guarantee
Guarantee是Promise的特殊情況,當我們確保事件不會有錯誤的時候,就可以用Guarantee來代替Promise,有它就不需要catch來捕獲異常了:
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,他不會影響事件的任何屬性,只是在適當的時機做一些不影響主線的事情,適用於打點:
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相似,但它語法的簡潔和直觀是它最大的特點,光是這一點就足夠吸引大家去喜歡它了~