Swift — Combine Publishers

讓我們一起來研讀 Combine 中的 Publishers 吧!

Jeremy Xue
Jeremy Xue ‘s Blog
10 min readJul 21, 2020

--

繼上一篇基本介紹 Combine 的官方語法後,這次我們將會著重探討 Combine 中一個重要的角色 — 發布者(Publishers),它在 Combine 中擔任發送資料的角色。然而在官方網站中也有幾個不太相同的 Publishers,就讓我們一一探討它們吧!

摘要:

  • Publisher
  • Publishers
  • AnyPublisher
  • Published
  • Cancellable
  • AnyCancellabe
  • Convenience Publisher
  • ConnectablePublisher
  • 通過可連接的發布者控制發布

✒︎ Publisher

宣告一個類型可以隨時間發送的序列值。

發布者傳遞元素到一個或多個訂閱者實例。訂閱者的 Input 以及 Failure 的關聯類型必須與發布者宣告的 Output 以及 Failure 的類型相匹配。發布者實現了 receive(subscriber:) 方法來接受訂閱者。

在這之後,發布者可以在訂閱者上調用以下方法:

  • receive(subscription:): 確認訂閱請求並且返回一個 Subscription 實例。訂閱者使用訂閱來向發布者需求元素,並且可以使用它來取消發布。
  • receive(_:):從發布者向訂閱者傳遞一個元素。
  • receive(completion:):通知訂閱者發布已正常結束或發生錯誤。

每個 Publisher 必須遵守此約定,下游訂閱者才能正常運作。

Publisher 上的擴展定義了各式各樣的運算符,你可以組合這些運算符來創建複雜的事件處理鏈。每個運算符都返回一個實現 Publisher 協議的類型,其中大多數的類型都作為 Publishers 枚舉的擴展存在。例如:map(_:) 運算符返回 Publishers.Map

創建自己的發布者

你可以使用 Combine 框架提供的以下幾種類型之一來創建自己的發布者,而不是自己實現 Publisher 協議:

  • 使用 Subject 的具體子類,像是 PassthroughSubject,透過調用其 send(_:) 方法按要求發布值。
  • 每當更新主題的底層值時,請使用 CurrentValueSubject 來發布。
  • 添加 @Publisher 註解到自己的一個類型的屬性中。如此一來,該屬性獲得一個發布者,該發布者會在屬性值發現變動時發出一個事件。

✒︎ Publishers

作為發布者類型的命名空間。

Publisher 上定義為擴展的各種運算符將它們的實現功能作為此枚舉的類或結構。例如: contains(_:) 運算符會返回 Publishers.Contains 實例。

與上面的 Publisher 不同,此 Publishers 為 enum,並且其中有許多的內嵌類型,像是 Pbulishers.Map、Publishers.Filter 等等,可以參閱此官方文件

✒︎ AnyPublisher

透過包裝另一個發布者執行類型擦除(type erasure)的發布者。

AnyPublisherPublisher 的具體實現,其本身沒有任何重要的屬性,並且會傳遞來自其上游發布者的元素和完成值。

使用 AnyPublihser 來包裝其類型包含你不想跨越 API 邊界暴露的細節的發布者,像是不同的 modules。使用 AnyPublisher 包裝 Subject 也可以防止調用者訪問其 send(_:) 方法。當你以這種方式使用類型擦除時,可以隨時間更改底層發布者實現,而不會影響到現有客戶端。

你可以使用 Combine 中的 eraseToAnyPublisher() 運算符來用 AnyPublisher 包裝發布者。

✒︎ Published

發布帶有特性標記屬性的類型。

發布具有 @Published 特性的屬性將創建這種類型的發布者。你可以使用 $ 運算符訪問發布者,如下所示:

當屬性更改時,發布會在屬性的 willSet 中發生,意味著訂閱者會在屬性實際設置新的值之前就接收到它。在上面的範例中,sink 第二次執行其閉包,它接收到參數值 25。但是,如果閉包評估了 weather.temperature,則返回的值為 20。

!重要:@Published 特性受類約束。
將它與類的屬性一起使用,而不是與非類類型(例如:結構)一起使用。

✒︎ Cancellable

一個表示活動或動作支持取消的協議。

調用 cancel() 釋放任何分配的資源。它還可以停止副作用,像是計時器、網路訪問或硬碟 I/O。

✒︎ AnyCancellable

類型擦除的可取消對象,在取消後將執行提供的閉包。

訂閱者實現可以使用此類型來提供 “取消 token“,其使調用者可以取消發布者,但不能使用 Subscription 對象請求項目。

反初始化後,AnyCancellable 實例會自動調用 cancel()

✒︎ Convenience Publishers

在 Combine 中還提供了一些 Convenience publishers,其中每個發布者各自都有不同的效果:

  • Future
    最終產生單個值,然後完成或失敗的發布者。
  • Just
    發布者只會向每個訂閱者發布一次輸出值,然後結束的發布者。
  • Deferred
    在運算提供的閉包為新訂閱者創建發布者之前,等待發布者的發布者。
  • Empty
    一個從不發布任何值的發布者,可以選擇立即完成。
  • Fail
    立即以指定的錯誤終止的發布者。
  • Record
    一個發布者允許記錄一系列的輸入和完成,給以後每個訂閱者回放。

這邊對於暫時對這些 Publisher 做一個簡單的介紹,之後會在實作各個不同 convience publisher 的效果。

✒︎ ConnectablePublisher

提供明確的連接和取消發布方式的發布者。

當需要在生成任何元素之前之前其他配置或設置時,請使用 ConnectablePublisher

在你調用其發布者的 connect() 方法之前,該發布者不會產生任何元素。

使用 makeConnectable() 來從 Failure 類型為 Never 的任何發布者創建一個 ConnectablePublisher

✒︎ 通過可連接的發布者控制發布

協調發布者開始向訂閱者發送元素的時間。

有時你想要在發布者開始產生元素之前對其進行配置,例如:當發布者具有影響其行為的屬性時。但是像是 sink(receiveValue:) 這樣常用的訂閱者會立即需要無限個元素,這可能阻止你按照自己的方式來設置發布者。當發布者有兩個或更多的訂閱者時,在你準備好產生值之前的發布者可能也會發生問題。這種多個訂閱者方案創建了一個競爭條件(race condition);發布者可以在第二個訂閱者甚至存在之前發送元素到第一個訂閱者。

考慮以下圖中的場景。你創建一個 URLSession.DataTaskPublisher 並且連接一個 sink 訂閱者到 Subscriber 1 上,這導致數據任務開始獲取 URL 的數據。稍後,你將連接第二個訂閱者 Subscriber 2。如果數據任務在第二個訂閱者連接之前完成其下載,則第二個訂閱者將丟失數據,僅會看到完成。

使用可連接的發布者來保持發布

為了防止發布者在準備就緒之前就發送元素,Combine 提供了 ConnectablePublisher 協議。可連接發布者在調用其 connect() 方法之前不會產生任何元素。即使它準備生產元素並且不滿足需求,可連結發布者也不會向訂閱者傳遞任何元素,除非你顯式的調用 connect()

下圖展示了上面 URLSession.DataTaskPublisher 的場景,但在訂閱者之前有一個 ConnectablePublisher。在兩個訂閱者連結之前一直等待調用 connect(),然後數據任務才開始下載。這消除了競爭條件,並且確保兩個訂閱者都可以接收數據。

要在自己的 Combine 程式碼中使用 ConnectablePublisher,請使用 makeConnectable() 操作符來將現有發布者包裝為 Publishers.MakeConnectable 實例。

以下程式碼展示了 makeConnectable() 如何修正上方描述的數據任務發布的競爭條件。通常,連接一個 sink(在這邊由 sink 所返回的 AnyCancellable 標識 — cancellable1)會導致數據任務立即啟動。這種場景下,被標識為 cancellable2 的第二個 sink 要等待一秒鐘過後才能連接,並且數據任務發布者可能會在第二個 sink 連接之前完成。相反的,顯式的使用 ConnectablePublisher 會導致數據任務只有在應用程序調用 connect() 之後才開始,其延遲後兩秒鐘後才開始。

因此,cancellable1cancellabel2 會同時接受到數據以及完成。

重要connect() 返回一個你需要保留 Cancellable 實例。你可以使用此實例來取消發布,透過顯式調用 cancel() 或允許它反初始化。

如果你不顯式連接,請使用自動連接運算符

某些 Combine 發布者已經實現了 ConnectablePublisher,像是 Publishers.MulticastTimer.TimerPublisher。使用這些發布者會導致相反的問題:如果你不需要配置發布者或連接多個訂閱者,必須顯式 調用 connect() 可能會很麻煩。對於此種狀況,ConnectablePublisher 提供了 autoconnect() 運算符。當訂閱者使用 subscribe(_:) 方法連接發布者時,此運算符立即調用 connect()。下面的範例使用 autoconnect(),因此訂閱者立即接收來自 Timer.TimerPublisher 每秒一次元素。如果沒有 autoconnect(),該範例將在需要時透過調用 connect() 顯式的啟動計時器發布者。

--

--

Jeremy Xue
Jeremy Xue ‘s Blog

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