【IOS - 使用PromiseKit優雅的來處理Callback hell(回呼地獄)】

Callback hell 這種情況並不是只會發生在 Swift 上,任何若是有牽扯到非同步機制的語言,都是會有這個狀況的。在開始說明如何處理 Callback hell 之前,先舉一個平常很容易遇到的狀況來說明何謂 Callback hell。例如現在有一個登入的頁面,而我們要在取得使用者輸入的帳號、密碼後呼叫後端的 API 來驗證是否正確,若是正確則緊接著要繼續呼叫下一個 API 來取得使用者的資料,然後啪啦啪啦的又要呼叫其它的 API 來做其它的事。而程式碼就會變的如同下面這樣一層接著一層往下呼叫(該範例使用 Alamofire 來實作之)。

而並沒有說這樣的方式是錯的,而是程式碼過於冗長且不易閱讀(你/妳可以想像一下每個 success 的 block 若又要進行很多很多的判斷),而今天法蘭克就要來介紹如何使用第三方的 library PromiseKit 來優雅的處理 Callback hell,讓非同步的程式碼看起來像是同步的程式碼。


前置作業:

  1. 了解何謂 Callback hell,這篇文章雖然是簡體文且大部分的範例都是使用 ObjectC 所撰寫的,但它的觀念可是非常到位的,先拜讀一下待會在看範例的時侯才不會一頭霧水。
  2. 了解 Swift 的 Closure 如何運作,因在實作的時侯會大量使用到 Closure,若是對 Closure 不怎麼熟的話,在實作上會比較難理解。
  3. PromiseKit 官方先稍微看過簡單的範例和說明。
  4. 知道如何使用 cocoaPods 來下載第三方的套件,若是不清楚的可看法蘭克之前所分享的文章。
  5. Demo 的 API 回傳結果欄位都是假設性的,實際上回傳的 API 並沒有 userId 的欄位,一切只是為了符合該教程而自行命名的欄位。

內容大鋼:

  1. 透過 cocoaPods 來安裝 PromiseKit。
  2. 使用基礎的方式來定義非同步的執行任務。
  3. 使用 when 來定義非同步的執行任務。
  4. 使用 firstly block 來定義非同步的執行任務。
  5. 演示實際在開發專案時會遇到的案例。

透過 cocoaPods 來安裝 PromiseKit

PromiseKit 整合了許多好用的第三方套件還有內建的 UIKit,而今天要介紹的範例會是使用 PromiseKit + Alamofire 的套件,還有常用到的 SwiftyJSON 套件。

▼先至 PromiseKit 整合 Alamofire 的 GitHub 上找到 cocoaPods 載入的指令後並貼到 podfile 上(PromiseKit 所有整合的套件清單)

▼至 SwiftyJSON GitHub 上找到 cocoaPods 載入的指令後並貼至 podfile 上

▼安裝完成後最後打開專案並 import 所有會用到的套件


使用基礎的方式來定義非同步的執行任務

安裝好所會用到的套件後,接下來就開始來簡化在一開始提及的驗證登入非同步 API,為了使程式碼看起來比較簡潔點以方便說明,所以這邊只會使用兩個 API 來說明之。

首先先將「驗證帳號、密碼是否正確」和「利用userId取得使用者的資料」這兩個 API 分開撰寫。

說明:
第一和第二個 func 的邏輯都是大同小異的,所以只針對第一個 func 說明之。

第 2 行 => 該 func 會回傳 Promise<T> 的物件,該物件封裝了所有的回傳結果,不管是成功或失敗的。這邊使用<String>的意思是因為會回傳型別為 String 的 userId。

第 3 行 => 若是成功則應呼叫 fulfill 這個閉包函式並將結果當成參數傳入;若是失敗則應呼叫 reject 這個閉包函式並將結果當成參數傳入。

第 4 行 => 使用 Alamofire 來驗證使用者輸入的帳號、密碼是否存在(API 只是假設性的並無真正驗證的功能)。

第 5~8 行 => API 服務正常並成功接收到回傳結果,呼叫 fulfill 閉包函式將結果存放至 Promise<T> 物件中,<T> 表示泛型,所以回傳結果型別是可以任意的,這邊回傳的 userId 是字串。

第 8~11 行 => 若是執行至此代表呼叫 API 失敗,失敗的原因有很多,例如 URL 錯誤或是服務未開啟等等都有可能。呼叫 rejcet 閉包函式拋出錯誤則會執行到 catch 的 block,以下會提及 catch 的 block。

當我們將以上兩個 func 實作完後,首先在全域變數宣告 JSON 型別的變數,該變數用來存放使用者資料。

接著在 viewDidLoad 宣告兩個變數,用來假設是的從畫面上取得到的使用者帳號和密碼。

緊接著開始使用最基礎的方式來執行兩個非同步的執行任務。

說明:

以上一共有四個 block,分別是 then、then、always、catch。

第一個 then => 呼叫驗證帳號密碼並在該 func 內呼叫 fulfill 閉包函式後所會執行到的 block,而 userId 則是 fulfill 閉包回傳的值。

第二個 then => 呼叫利用使用者 ID 來取得使用者資料並在該 func 內呼叫 fulfill 閉包函式後所會執行到的 block,而 userData 則是 fulfill 閉包回傳的值,將其存放於全域變數。

always => 顧名思義就是總會被執行的 block,不管前面所有的 API 結果為何一定都會被執行到,就算前面的 API 有執行到 reject 閉包函式一樣會執行到該 block。而這邊會判斷若是 userData 有值則會將值 print 出來。該 block 為非必要的,可有可無,只是為了要說明有 alwsay blcok 可使用。

catch => 有錯誤發生時,即 API 呼叫 reject 的閉包函式。error 可自行定義,後面會說明之。

以上都撰寫完成後,試著執行看看結果是不是如預期中的順序。

第一個 then => 第二個 then => always

備註:試著將 API 的參數改成空字串,試試結果為何?可藉此加深印象。


使用 when 來定義非同步的執行任務

when 看起來更簡潔了一些,它可以用於定義當數個非同步任務執行完後,才要緊接著執行其它任務的 block。

說明:

第 1~2 行 => 宣告執行「驗證帳號、密碼是否正確」和「利用userId取得使用者的資料」並存放於變數中待使用 when 來實作它們。

第 4 行 => 呼叫 when 方法並將 Promise<T> 型別的物件當成參數傳入。

第 5 行 => 當「驗證帳號、密碼是否正確」和「利用userId取得使用者的資料」的任務都執行完後,會執行到的 block。


使用 firstly block 來定義非同步的執行任務

firstly 的靈活度可算是最高的,firstly block 可撰寫在執行非同步任務前的一些其它邏輯。

說明:

第 2~4 行 => firstly block,第 3 行在執行非同步任務時開始執行 ActivityIndicatior 以讓使用者覺得應用程式有在執行某些事情,第 4 行則呼叫 API 驗證帳號、密碼是否正確。

第 6 行 => 當驗證帳號、密碼呼叫 fulfill 的閉包函式時,就會執行到該 block 開始用使用者的 userId 取得使用者的資料。

第 8 行 => 將使用者的資料存放於全域變數。

第 10~13 行 => 第 10 行不管執行成功與否都要停止 ActivityIndicatior,第 12 行若是使用者資料不為空則將結果輸出。


演示實際在開發專案時會遇到的案側

首先必須調整 checkAccountAndPassword() 和自定義 Error Enum 以用來演示在實際開發專案會遇到的狀況。

說明:
第 2~5 行 => 透過遵守 Error 這個協議來自定義錯誤。

第 13~22 行 => 利用 SwiftyJSON 套件取出 son 格式的資料,並判斷是否驗證成功。若驗證成功則將 userId 回傳,若驗證失敗則回傳自定義的錯誤。

第 45~56 行 => 處理錯誤邏輯,透過自定義的錯誤就能清楚在呼叫 API 的過程中到底發生了什麼錯誤。


結論:

該教程已放上 GitHub 供下載,若是在教程中有任何說明錯誤的地方,請留言給法蘭克。

Show your support

Clapping shows how much you appreciated 法蘭克的IOS世界’s story.