Swift — 使用 Combine 處理 URL 任務

讓我們看看如何使用 Combine 來處理常見的 URL 任務吧!

Jeremy Xue
Jeremy Xue ‘s Blog
7 min readAug 23, 2020

--

使用一系列的異步運算符來接收和處理來自 URL 的數據。

使用 URLSession 執行任務本質上是異步的,它從網路端點、文件系統和其他 URL 來源中獲取數據需要花費時間。URL 加載系統透過將結果異步傳遞給 delegate 或 completion handler 來解決此問題。Combine 框架還處理了異步性;使用它來處理你的 URL 任務結果可以簡化並授權你的程式碼。

創建數據任務發布者:

URLSession 提供一個 Combine 發布者 — URLSession.DataTaskPublisher,該發布者從 URLURLRequest 獲取數據的結果。你可以使用 dataTaskPublisher(for:) 來創建此發布者。任務完成後,它將發布以下任一內容:

  • 如果任務成功,則包含獲取的數據和 URLResponse 的元組。
  • 如果任務失敗,則是一個錯誤。

與傳遞給 dataTask(with:completionHandler:)completion handler 不同,程式碼所接收的類型不是可選的,因為發布者已經解包數據或錯誤。

當使用 URLSession 基於 completion handler 的程式碼時,你必須在其閉包中完成所有工作:錯誤處理、數據解析等等。當你改用數據任務發布者時,可以將其中許多職責移動至 Combine 運算符中。

使用 Combine 運算符將原始數據轉換為你的類型:

當數據任務完成後,它將原始數據發送至你的應用中。大多數的應用需要將此數據轉換為自訂的類型。Combine 提供了運算符來執行這些轉換,允許你宣告一系列的處理運算。

數據任務發布者產生一個包含 DataURLResponse 的元組。你可以使用 map(_:) 運算符將此元組的內容轉換為另一個類型。如果你想要在檢查數據之前檢查回應,使用 tryMap(_:) 並且在回應為不可接受時拋出錯誤。

要將原始數據轉換為自己符合 Decodable 協議的類型,請使用 Combine 的 decode(type:decoder:) 運算符。

以下範例結合了這兩個運算符來將 URL 端點中的 JSON 數據解析為自定義的 User 類型:

重試暫時性錯誤以及捕獲和替換持續性錯誤:

任何使用到網路的應用都應該預期會遭遇錯誤,並且你的應用應該能夠優雅的處理它們。由於暫時性的網路錯誤相當普遍,因此你可能需要立即重試失敗的數據任務。使用 URLSession 的 completion handler 慣用語法,你需要創建一個全新的任務來執行重試。使用數據任務發布者,你可以改用 Combine 的 retry(_:) 運算符。這透過重新創建上游發布者的訂閱指定次數來處理錯誤。但是,由於網路運算成本很高,因此只能重試幾次,並確保所有請求都是冪等(idempotent)的。

你還可以使用 Combine 運算符來替換錯誤,而不是讓錯誤傳達到訂閱者:

  • catch(_:):將錯誤替換為另一個發布者。你可以與其它 URLSession.DataTaskPublisher 使用一起使用,例如從後備 URL 加載數據。
  • replaceError(with:):用你提供的元素替換錯誤。如果在你的應用中有意義,則可以使用它來替代你期望從 URL 加載的值。

使用排程運算符在調度隊列之間移動工作:

當使用 URLSession 的 delegate 和 completion hanlder 慣用語法時,URLSession 將會在固定的委託隊列上回調你的程式碼。有時,這意味著你的回調程式碼必須手動使用調度隊列(Dispatch Queue)或其他調度 API 將工作放到特定隊列上。

使用 URLSesion.DataTaskPublisher,你可以改用 Combine 的排程運算符(scheduling operators)。使用 receive(on:options:) 來指定你希望鏈中之後的運算符和訂閱者如何安排工作。DispatchQueueRunLoop 兩者都實現了 Combine 的 Scheduler 協議,因此你可以使用它來們接收 URLSession 數據。以下程式碼片段確保 sink 將其結果紀錄在主調用隊列上:

與多個訂閱者共享數據任務發布者的結果:

你可能想在應用中的不同部分使用來自 URL 端點的數據。由於網路請求的成本很高,因此請勿不必要的重新發送。Combine 使你可以使用多個訂閱者訪問到單個 URLSession.DataTaskPublisher,同時允許發布者透過單個請求為所有訂閱者提供服務。

要支持多個下游訂閱者,請使用 share() 運算符。此運算符的工作方式類似於 Publishers.MulticastPassthroughSubject 發布者的組合。你可以將多個運算符鏈或訂閱者連接到 share() 運算符,任何上游發布者只能看到一個下游。對於 URLSession.DataTaskPublisher,這意味著它只有執行一次數據任務。

以下範例使用 URLSession 數據任務用於兩個不相關的目的。一個訂閱者使用返回的數據來解析之前看過的自定義 User 類型,並將其結果記錄在主調度隊列中。第二個訂閱者只關心 URLResponse,對其進行檢查來印出 HTTP 狀態碼,而不關心使用在哪個隊列。透過使用 share(),數據任務發布者可以從 URL 端點單次加載就為兩個訂閱者提供服務。

為了證明此程式碼只有加載一次數據,請在 share() 運算符之前暫時放置一個 print(_:to:) 調試運算符。當該應用運行時,即使兩個訂閱者都收到了預期的結果,Xcode 的控制台輸出也顯示它僅從數據任務發布者那裡接收到了一個值。

請注意,當 URLSession.DataTaskPublisher 具有來自下游訂閱者未滿足的要求時 URLSession 就會開始加載數據 。在這種情況下,這將在第一個 sink 訂閱者連接時發生。如果你需要額外的時間來連接其他訂閱者,請使用 makeConnectable() 來將 Publishers.Share 發布者包裝為 ConnectablePublisher。在連接所有訂閱者後,請在可連接的發布者上調用 connect() 來開始加載數據。

--

--

Jeremy Xue
Jeremy Xue ‘s Blog

Hi, I’m Jeremy. [好想工作室 — iOS Developer]