Swift. Observer & Notifications

讓我們來看看如何使用 Notification 與觀察者模式吧!

Jeremy Xue
Jeremy Xue ‘s Blog

--

Photo by Mohammad Metri on Unsplash

▸ 前言:

在設計模式中,我們有一個稱為觀察者(Observer)的模式,我們可以設計出一個訂閱/發布系統,我們大致上可以分為兩個對象 — 發布者與觀察者。在許多模式的概念下都會有這兩個角色,發布者負責發出通知給觀察的對象,而觀察者負責訂閱發布者的服務,並且在接收到通知時做出相應操作。

而在 iOS,我們可以透過 NotificationCenter 來很容易的實現此模式。其中,觀察者可以透過其 addObserver 方法來作為某個通知的觀察者,發布者可以透過 post 方法來發布某個通知的事件,因此,在 iOS 的 Notifications 指的是此模式,而非推播(Push Notification)。

注意:但這邊我認為 Notifications 與 Observer Pattern 的概念上還是略有不同。我認為的 Observer Pattern 的發布者中會管理一組觀察(或訂閱)的對象,當需要發布通知時,他會對這些觀察者發布通知。而 iOS 中的 Notifications 實際上比較類似於廣播,發布者會廣播通知(並不是針對某些對象來發送通知),而有興趣對象可以訂閱該廣播通知成為觀察者,就能收到該通知廣播。

▸ 觀察者模式(Observer Pattern)

首先,我們簡單來實作一個訂閱,其實有點類似於 Delegation 的概念,只是 Delegation 大部分使用在一對一的狀況下,而 Observer 的通常適用於一對多或是多對多的狀況。

關於 Delegation 可以參考我上一篇文章:

因此,我們可以用 Delegation 的概念來編寫我們的 Observar(至於為什麼要抽象的原因也是相同),只是我們的委託對象可以是多個(陣列),之後我們就能夠發送通知給這些觀察者:

而要成為 Store 物件的觀察者,只要遵循 StoreObserver 協議即可:

因為,我們 Store 中的 observers 的型別被抽象為 StoreObserver。因此,只要是符合 StoreObserver 的對象都能被加入 observers 接收到訊息。

如此一來我們就完成一個簡單的 Observer 模式的實現了。

而在我們設計 Observer 時,也必須考慮強引用循環的問題,所以應該對每個觀察者保持著弱引用。而 Swift 中的 Collection 默認情況下都為強引用,所以我們會新增一個作為包裝器的類型,只用來保持對觀察者的弱引用:

而對於 observers 的部分,我們將從陣列改為字典,以 ObjectIdentifier 作為 Key 的類型,而以 Observation 作為 Value 的類型。這邊透過將 ObjectIdentifier 作為 Key,在做刪除 observers 時可以辨別是否為同一個實例。

接著我們再次執行上述內容,當我們物件設為 nil 時,該實例就會被釋放:

我們也可以在遍歷 observers 時,同時把找不到的 observer 給移除:

▸ Notifications

接著,我們使用 Notification 來實作 Observer 模式的效果。我們透過 NotificationCenter 裡面提供的兩個方法即可達成,分別是:

  • post
  • addObserver
  • removeObserver(optional)
*需要主動移除 observer 嗎?如果您的應用在 iOS 9.0 或 macOS 10.11 更高版本,則無需主動移除觀察者。如果您忘記或無法刪除觀察者,系統會在下次 post 時進行清理。

首先,讓我們先看看發送通知的 post 方法:

而其中 post 的參數代表的是:

  • name:通知名稱
  • object:發布通知的物件(Optional)
  • userInfo:通知相關的用戶資訊(Optional)

而要作為通知的觀察者,我們可以透過 addObserver 的方式來註冊:

其中,兩個 addObserver 方法相同的函數有以下幾個:

  • observer:要註冊為觀察者的對象
  • name:註冊的通知名稱。當為 nil 時,發送者不使用通知名稱作為傳遞標準。
  • object:向觀察者發送通知的物件。當為 nil 時,NotificationCenter 不使用發送者名稱作為發送標準。

SomeObject 中我們透過 selector 參數指定接收通知的方法,可以接收類型為 NSNotification 的實例。而 AnotherObject 中我們透過 block 參數提供的閉包處理接收到通知時的處理,並且可以透過 queue 參數來指定閉包的運行佇列,若為 nil,則會與發佈者的相同線程上同步運行。

接著,我們運行看看是否一樣能夠接收到 Store 所發布的通知:

而在移除觀察者的部分,我們可以透過以下的方式刪除:

而帶有參數的 removeObserver 其中的兩個參數的意義如下:

  • observer:要移除的觀察者
  • name:要刪除的通知名稱。指定通知名稱來刪除具有此通知名稱的項目。當為 nil 時,接收者不使用通知名稱作為刪除標準。
  • object:要刪除的發布者。指定通知發送者來刪除具有此發送者的項目。當為 nil 時,接收者不使用發送者作為刪除標準。

你也可以不帶任何參數來刪除 NotificationCetner 中的指定觀察者:

而如果是使用帶有閉包的 addObserver 來新增觀察者,你也可以透過其方法所返回的 NSObjectProtocol 來移除他。

你還可以透過這個方式實作一次性的通知:

如此一來,在我們移除觀察者後,就不會再收到發布者所發佈的通知了:

▸ Notifications 封裝

雖然 Notifications 看起來非常容易就能達到發布/訂閱的概念,但是我們有明顯的幾個問題需要處理:

  • 管理 Notification 名稱(字串)
  • 處理接收到的 Notification 的 uesrInfo

首先,我們先來處理 Notification 名稱。其實非常容易,這邊示範一個常見的用法,我們透過 extension 擴展 Notification.Name

如此一來使用到它的地方將會變得非常簡潔。我們只需要 .postMessage 就能使用:

而關於如果處理 userInfo 內的資訊就有蠻多方式的,就看讀者們想要如何封裝。舉個簡單的封裝範例:

我們可以透過把所有實作細節封裝於發佈者中,如此一來外部的人只需呼叫我們所提供的方式來發送訊息與添加觀察即可。而 notificationNameuserInfoKey 也可隱藏於其中,防止混用。使用方式如下:

而另一個方式我也蠻喜歡的,我是參考「客製化 NotificationCenter 讓你使用起來更簡單」此文章中的概念來改寫。他有點像是抽象出一個可被作為通知的物件,如此一來就可以處理從發送到解析的過程,

透過 Notificationable 我們可以默認生成與自身名稱相同的 notificationName、 固定的 userInfoKey 以及可以透過 Notification 初始化自身的可失敗初始化器。

接著,我們透過 extension 擴展 NotificationCenter 來添加使用方法:

首先,我們 post 方法透過 Notificationable 協議後,可以獲取其中的 notificationName,而傳送的 userInfo 部份也可以透過獲取其 userInfoKey 以及其 Notificationable 本身組合。而 addObserver 方法,我們一樣透過 Notificationable 來獲取 notificationName,並且在收到通知後初始化為 Notificationable 本身實例後,透過 block 閉包傳遞出去。

接著,我們就可以透過以下方式來快速的發送以及訂閱特定物件的通知:

--

--

Jeremy Xue
Jeremy Xue ‘s Blog

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