Swift — Combine 入門介紹

讓我們一起共同踏進 Combine 的世界吧!

Jeremy Xue
Jeremy Xue ‘s Blog
8 min readJul 19, 2020

--

在 2019 年時,Apple 推出了兩套強大的框架分別是 SwiftUI 以及 Combine,而這兩種全新的框架對於開發者來說是一個重大的改動,它也跟以往我們熟悉的編成方式不同,因此要能夠熟悉這兩個新推出的框架也算是一個大挑戰。

而最近剛好在到處看看一些關於架構或是設計模式的文章,舉凡:MVVM、MVVMC、Coordinator、Redux 等等…。而看這些技術文章無非都是希望能在對於自己的程式碼有多一點的要求,並且處理數據或流程上更加嚴謹。

然而在學習到 Observer Patterm 的時候有許多的感觸,這種觀察者(響應式)的做法讓我們在處理數據時有更多的想法,又加上 Apple 去年也推出了 Reactive 的框架 — Combine,因此就想說藉著這個機會逼自己學習這個框架,而這邊文章主要會介紹 Combine 框架中一些重要的成員。

Note:
因為自己也是剛從零開始學習 Combine 的,可能沒有相關的實作經驗,
如果有任何錯誤或是誤解再請各位讀者幫我導正觀念,感謝 🙏。

摘要:

  • Combine 概論
  • 使用 Combine 接收和處理事件

✒︎ Combine

透過組合事件處理運算符來自訂異步事件的處理。

Combine 框架提供了一個宣告性(declarative)的 Swift API,其用於隨時間推移的值處理,這些值可以表示多種異步事件。Combine 宣告發布者(Publishers)去暴露隨時間變化的值,而訂閱者(Subscribers)去從發布者那裡接收這些值。

  • Publisher 協議宣告了一個可以隨時間傳遞序列值的類型。發布者具有運算符(Operators)來從上游發布者那裡獲得的值採取行動,然後重新發布他們。
  • 在發布者鏈的結尾,訂閱者在接收元素的時候對其進行操作。發布者只能在訂閱者明確要求時才發出值。如此一來,你的訂閱者程式碼就可以控制從連接的發布者那裡接受事件的速度。

幾種 Foundation 的類型透過發布者暴露其功能,包括 TimerNotificationCenter 以及 URLSession。Combine 還提供符合 KVO(Key-Value Observing) 的任何屬性提供了內建發布者。

你可以合併多個發布者的輸出並且協調它們的交互。例如:你可以訂閱來自 text field 的發布者更新,然後使用文字執行 URL 請求。然後,你可以使用另一個發布者來處理回應,並且使用它們來更新你的應用。

透過採用 Combine,你可以透過集中事件處理程式碼以及消除麻煩的技術(像是 nested closures 或是 convention-based callbacks)來使你的程式碼更容易閱讀以及維護。

✒︎ 使用 Combine 接收和處理事件

自定義和接收來自異步來源的事件。

Combine 框架為你的應用處理事件提供了一種宣告性方式。你可以為給定的事件來源創建單個處理鏈,而不是潛在的實現多個委託 callbacks 或 completion handler 閉包。鏈中的每個部分都是一個 Combine 運算符,它對從上一步收到的元素執行不同的操作。

考慮一個需要根據 text field 的內容來過濾 table/collection view 的應用。在 AppKit 中,text field 中的每個擊鍵都會生成一個 Notification,你可以用 Combine 訂閱它。收到通知後,你可以使用操作符來更改事件傳遞的內容以及時間點,並且使用最終結果來更新使用者介面。

將 Publisher 連接到 Subscriber

要使用 Combine 接收 text field 的通知,請訪問 NotificationCenter 的默認實例,並且調用其 pushlisher(for:object:) 方法。此調用採用你要從其接收通知的通知名稱以及來源對象,並返回產生通知元素的發布者。

你使用 Subscriber 來接收來自發布者的元素。訂閱者定義了一個關聯類型 — Input 來宣告其接收的類型。而發布者也定義了一個關聯類型 — Output,來宣告其產生的內容。發布者和訂閱者兩者都定義了一種類型 — Failure 來表示他們產生或接收的錯誤的類型。要將訂閱者連接到發布者,則 Output 必須匹配 Input,並且 Failure 的類型也必須互相匹配。

Combine 提供了兩個內建的訂閱者,這些訂閱者自動匹配其附屬發布者的輸出和失敗類型:

  • sink(receiveCompletion:receiveValue:) 採用兩個必包。第一個閉包在收到 Subscribers.Completion 時執行,這是一個枚舉,其表示發布者是正常完成還是因為錯誤而失敗。第二個閉包在收到發布者的元素時執行。
  • assign(to:on:) 使用 key path 來表示屬性立即將其接收到的每個元素分配給給定對象的屬性。

例如,你可以使用 sink 訂閱者來印出每次發布者完成以及每次收到元素時的 log:

sinkassign 兩種訂閱者都向其發布者請求無限數量的元素。想要控制接收元素的比率,請透過實現 Subscriber 協議來創建自己的訂閱者。

使用運算符更改輸出類型

上面的 sink 訂閱者在 receiveValue 閉包中執行其所有操作。如果他需要對接收到的元素執行很多自定義操作或在調用之間保持狀態時,這可能會很麻煩。而 Combine 的優點來自結合運算符來自定義事件傳遞。

例如,如果你需要的只是 text field 的 string 值,而NotificationCenter.Publisher.Output 並不是一個在 callback 中方便接收的類型。由於發布者的輸出是一段時間內的一個序列元素,因此 Combine 提供了可修改序列的運算符,像是:map(_:)flatMap(maxPublishers:_:)reduce(_:_:)。這些運算符的行為類似於 Swift 標準庫中的同樣行為。

要更改發布者的輸出類型,請添加一個 map(_:) 運算符,其閉包將返回不同的類型。在這種情況下,你可以將通知的對象獲取為一個 UITextFiled,然後獲取其 text

在發布者產生所需的類型後,將 sink(receiveCompletion:receiveValue:) 替換為 assign(to:on:)。下面的範例獲取從發布者鏈接收到的字串,並將其分配給自定義的 ViewModel 對象的 fillterString

使用操作符自定義發布者

你可以使用執行某些行為的運算符來擴展 Publisher 實例,否則這些操作需要手動進行編碼。你可以使用以下三個方式來使運算符改善此事件處理鏈:

  • 可以使用 filter(_:) 運算符來忽略特定長度下的輸入或是拒絕非字母數字的字,而不是使用在 text filed 中輸入任何內容時更新 ViewModel。
  • 如果過濾運算很昂貴(例如:查詢大型數據庫),則可能要等待用戶停止輸入。為此,我們可以使用 debounce(for:scheduler:options:) 運算符可以設置發布者發出事件之前必須經過的最短時間。RunLoop 類為指定以秒或毫秒為單位的延遲時間提供了方便。
  • 如果結果更新了 UI,則可以透過調用 receive(on:options:) 方法 callback 回主線程。透過將 RunLoop 類提供的 Scheduler 實例指定為第一個參數,可以告訴 Combine 在主運算循環上調用你的訂閱者。

結果發布者宣告如下:

需要時取消發布

發布者持續發出元素,直到其正常完成或失敗為止。如果你不想再訂閱發布者,則可以取消訂閱。由 sinkassign 所創建的訂閱者都實現的 Cancellable 協議,該協議提供了 cancel() 方法:

如果創建自定義 Subscriber,則發布者在首次訂閱時會發送一個 Subscription 對象。存儲此訂閱,然後再要取消發布的時候調用其 cancel() 方法。當創建自定義訂閱者時,你應該要實現 Cancellable 協議,並讓你的 cancel() 實現將調用轉發給存儲的訂閱。

--

--

Jeremy Xue
Jeremy Xue ‘s Blog

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