客製化 NotificationCenter 讓你使用起來更簡單


觀察者模式是一個常見、而且歷史悠久的程式設計模式,而在 Swift 裡,它主要是以通知與通知中心 (NotificationCenter) 的形式存在的。簡單來說,物件可以去向通知中心註冊,成為某一種通知事件的觀察者,然後當有人向通知中心送出通知的時候,通知中心就會去找它的註冊表裡面,所有有註冊這個通知類型的觀察者,並將通知傳送給它們。

通知中心模式跟 Target-action 模式與 Delegate 模式一樣,都是 iOS 開發裡不可或缺的一部分。像是要應對軟體鍵盤彈出的話,就得要去註冊相關的通知才行。然而,它有一個很麻煩的地方是:它的 API 有點囉唆。

原本的通知中心

NotificationCenter 是一個非常有 Objective-C 風格的 API。在早期版本的 Swift 中,它多數方法的參數都是 String 或者 Any,直到後來才有了 Notification.Name 等的新型別去包裝這些字串。但即使如此,它用起來還是感覺有些礙手礙腳。

通知中心用來辨別通知類型的方法,是透過它的名稱,所以你必須要找到相對應的名稱才行。官方框架的部分名稱是直接歸在 Notification.Name 這個命名空間底下,很好找,但有其它更多的名稱四散在各處,不看文件很難找得到。

送出通知有時也只需要一行,似乎很簡潔對不對?但魔鬼就藏在細節裡!userInfo 這個參數所接收的型別是 [AnyHashable: Any],所以如果要讓通知夾帶一些資訊的話,就必須要把資訊包裝到一個 Dictionary 裡面去,而這可以說是最沒有效率的包裝法了。送出通知的時候或許還不覺得,但接收的時候要解開包裝就麻煩了。

首先,要知道用來存放 someInfo 的字典鍵 (key) 是甚麼(這裡是字串 "SomeInfo")。接著,還要將它從 Any 轉型成 SomeType 才能用。也就是說,光是要取出一個值 (value),就要知道分開的兩個資訊(鍵與型別)。對於官方框架的通知來說,我們只能查文件去了解甚麼鍵值對應到甚麼型別的資訊;而如果通知是我們自己發送、userInfo 是我們自己打包的話,每一個自訂的通知就相當於要管理 :

  1. 通知名稱
  2. userInfo 鍵值對的鍵
  3. 鍵值對的值的型別

等資訊。而如果 userInfo 的鍵值對不只一個,要管理的資訊又更多了。

登場:Swift 的型別系統

這樣子存放附帶的資訊,真的是浪費了 Swift 強大的型別系統。在 Swift 裡,我們可以用 structenum 去定義資料結構,用 class 去定義物件,或用 protocol 去定義協定等等。重點是,所有這些型別都會經過編譯器的型別檢查

比如說,原本當 userInfo[AnyHashable: Any] 字典型別的時候,我們需要用字串去取值,取了值之後還要再轉換型別:

但如果今天 userInfo 是定義成一個 struct,而不是一個 Dictionary 的話,像這樣:

Swift 編譯器就會知道它有哪些成員,分別是甚麼型別:

我們不用再去找字典鍵、不用猜它的型別,userInfo.foo 直接就是 Bar 型別的值,甚至連解開包裝的動作都不用做。這樣不是很好嗎?

我們今天就是要來把通知中心模式中,這些弱型別的元素都轉化成 Swift 的強型別系統。

打包!全都用 Structure 打包起來!

首先,先想辦法把 userInfo 從字典轉成其它的型別,免去字典鍵管理與型別轉換的麻煩。這其實很容易達到,只需要把相關的資訊全部打包到一個型別裡面就可以了。

雖然還是需要做轉型,但只需要做一次就可以獲得所有資訊,已經算是進步了。不過,如果拿到處理通知的方法裡面來看的話,還是有點囉唆:

這時我們只要幫 PeopleInfo 加個便利的 init,把重複性高的程式碼丟進去:

就可以把剛剛的程式碼縮減成這樣:

問號都不見了,infoKey 不見了,也不用再轉型了,是不是簡潔很多呢?

不過,現在還只簡化到接收通知的部分。傳送通知的時候,除了要打包 userInfo 之外,還需要傳入通知的名稱:

這時就可以用 Extension 去給 NotificationCenter 加個方便的方法了:

以後要傳送通知的時候,就可以這樣寫:

這樣夠不夠簡單呢?

到現在為止,新增的程式碼長這樣:

這樣,就可以大幅簡化發送 PeopleInfo 的通知過程了。然而,它有一個極大的缺點 —— 就是所有這些程式碼,都只影響到 PeopleInfo 所代表的通知而已。如果想要使另一種類的通知也被簡化的話,就只能把這些程式碼複製一份過去新的包裝型別裡面了⋯⋯ 真的是這樣嗎?

Protocol Extension 來拯救大家了

在 Swift 裡,給一個型別直接加上方法與屬性的方式有幾種:

  1. 直接寫在主要宣告內(不適用於官方或第三方等,無法更動原始碼的型別)
  2. 寫在 Extension 內
  3. 寫在父類型內
  4. 寫在 Protocol Extension 內

就這裡的狀況來說,1 跟 2 都是需要針對每一個通知類型都寫一次大致相同的程式碼。寫在父類的話,又只支援 Class 型別,沒辦法用在 Structure 或 Enumeratoin 等無繼承的型別上面。最後,只剩 Protocol Extension 來拯救這一天了!

PeopleInfo 的擴充功能用 Protocol Extension 來改寫其實並不難,只要稍微改幾個字就可以了:

至於傳送通知的方法,則可能更複雜一點,要動用到通用型別 (Generics):

那使用起來如何呢?

由於新的 NotificationRepresentable 協定沒有任何的需求,所以要使原本的 PeopleInfo 遵守它,只需要加一行就可以了:

只要有了這一行,就可以直接使用所有剛剛寫的便利方法了!夠方便嗎?

不過,如果有很多種通知的話,那是不是就也要宣告很多新的包裝型別了呢?

Enumeration 也來參一腳

不用,因為可以用 Enumeration!我們可以定義一個 enum 去把不同的通知種類列成不同的 case,然後把所有類似的通知,都註冊到同一個接收通知的方法上面。這樣的話,就可以在接收到通知之後,去用 switch 切換不同種類的通知處理了。

這樣子的通知中心,是不是更吸引人呢?

至於向通知中心註冊的便利方法,就留給你自己去實作了。

(提示:用通用型別抽換掉某個參數看看!)


iOS 開發者、寫作者、filmmaker。現正負責開發 Storyboards by narrativesaw 此一故事板文件 app 中。深深認同 Swift 對於程式碼易讀性的重視。個人網站:lihenghsu.com。電郵:[email protected]

blog comments powered by Disqus
訂閲電子報

訂閲電子報

AppCoda致力於發佈優質iOS程式教學,你不必每天上站,輸入你的電子郵件地址訂閱網站的最新教學文章。每當有新文章發佈,我們會使用電子郵件通知你。

已收你的指示。請你檢查你的電郵,我們已寄出一封認證信,點擊信中鏈結才算完成訂閱。

Shares
Share This