iOS 中跨頁面狀態同步方案比較
由於團隊希望專案能夠去 CoreData 化,而以往狀態同步都是依賴於 CoreData 的 NSFetchedResultsController
。因此去 CoreData 則必須尋找一種替代方案來進行狀態同步。
NotificationCenter
狀態同步實際是一對多的場景,也就是一個事件可以被多個觀察者監聽到。而蘋果的系統框架自帶的 NotificationCenter 正是用來適配這種場景,並且其也是被系統框架本身及我們開發者大面積使用的。用法如下:
- 定義通知名字,以及需要額外傳遞資訊的 key
- 基於 target-action 的方式註冊通知
open func addObserver (_ observer: Any,selector aSelector: Selector,name aName: NSNotification.Name?,object anObject: Any?)
複製程式碼
- 實現監聽通知的方法
func onReceivedNotification(note: NSNotification)
複製程式碼
- 傳送通知,可以傳遞傳送通知的物件(object)以及一些額外的資訊(userInfo)
open func post(name aName: NSNotification.Name,object anObject: Any?,userInfo aUserInfo: [AnyHashable : Any]? = nil )
複製程式碼
- 移除註冊的通知
open func removeObserver(_ observer: Any,object anObject: Any?)
複製程式碼
當然 NotificationCenter 也提供了一種更加便利基於 block 的方式註冊監聽通知,其將 2,3 兩個步驟整合為 1 個步驟。
open func addObserver(forName name: NSNotification.Name?,object obj: Any?,queue: OperationQueue?,using block: @escaping (Notification) -> Void ) -> NSObjectProtocol
複製程式碼
整體流程很清晰,簡單易用,但是卻有一個嚴重的缺點 —— 弱型別。我們接收到的是一個 NSNotification
物件。
open class NSNotification : NSObject,NSCopying,NSCoding {
open var name: NSNotification.Name { get }
open var object: Any? { get }
open var userInfo: [AnyHashable : Any]? { get }
}
複製程式碼
假設我們需要傳遞一個關注狀態改變的資訊,那麼需要包含關注更改後的狀態以及被關注者的 ID。那麼我們需要從 userInfo 中取出所需要的值:
let following = notification.userInfo?["FollowingKey"] as! NSNumber
let userID = notification.userInfo?["UserIDKey"] as! NSNumber;
複製程式碼
也就是說接收通知的一方一般需要要檢視文件才知道怎樣從 userInfo 取值,取的值的型別又是什麼。這對於使用是極為不方便的。
SwiftNotificationCenter
SwiftNotificationCenter 是一種面向協議的通知中心方案。使用方式如下:
- 定義協議
protocol FollowingChanged {
func followingDidChange(following: Bool,userID: NSNumber)
}
複製程式碼
- 基於協議註冊通知
Broadcaster.register(FollowingChanged.self,observer: observer)
複製程式碼
- 實現協議方法
extension ViewController: FollowingChanged {
func followingDidChange(following: Bool,userID: NSNumber) {
// do something
}
}
複製程式碼
- 傳送通知
Broadcaster.notify(FollowingChanged.self) {
$0.followingDidChange(following,userID)
}
複製程式碼
- 移除註冊的通知
Broadcaster.unregister(FollowingChanged.self,observer: observer)
複製程式碼
我們可以看到,其基於協議的方式解決了弱型別的問題,但其也存在著擴充套件性較差的問題。
依然是關注改變的場景,假如隨著業務的發展,有的地方需要知道關注後是否為互關的狀態,那麼又需要增加一個欄位來標識。因此我們需要修改協議,增加引數 followingEachOther,且由於它不是必須傳遞的引數,因此是 optional 型別。
protocol FollowingChanged {
func followingDidChange(following: Bool,userID: NSNumber,followingEachOther: NSNumber?)
}
複製程式碼
如果在該型別通知被廣泛應用的場景,那麼需要修改的地方就尤其多了。這顯然也是難以接受的。
EventBus
EventBus 在安卓中被廣泛地應用,其流程如下圖所示:
圖片來源:EventBus使用方式如下:
- 定義事件
class TPFollowingChangedEvent: NSObject,TPEvent {
private(set) var following: Bool
private(set) var userID: NSNumber
}
複製程式碼
- 註冊事件
TPEventBus<TPFollowingChangedEvent>.shared.register(eventType: TPFollowingChangedEvent.self,subscriber: self,selector: #selector(onEvent(event:object:)))
複製程式碼
- 實現監聽事件的方法
@objc func onEvent(event: TPFollowingChangedEvent,object: Any?) {
// do something
}
複製程式碼
- 傳送事件
TPEventBus.shared.post(event: event,object: self)
複製程式碼
- 移除事件的註冊
TPEventBus<TPFollowingChangedEvent>.shared.unregister(eventType: TPFollowingChangedEvent.self,subscriber: self)
複製程式碼
我們可以看到, EventBus 也是強型別的。
假如依然是關注的場景,需要知道互關的狀態,那麼我們只需要在 TPFollowingChangedEvent 中增加 followingEachOther 屬性即可。如下所示:
class TPFollowingChangedEvent: NSObject,TPEvent {
private(set) var following: Bool
private(set) var userID: NSNumber
private(set) var followingEachOther: NSNumber?
}
複製程式碼
因此使用 EventBus 實現了以下需求:
- 強型別
- 可擴充套件
EventBus 同 NotificationCenter 都是基於 target-action 的方案,但是我們不難將其擴充套件為支援 block 監聽的方式,並且同樣讓它也能夠自動移除事件的註冊。類似於如下的使用方式:
TPEventBus<TPFollowingChangedEvent>.shared.subscribe(eventType: TPFollowingChangedEvent.self).forObject(self).onQueue(OperationQueue.main).onEvent { (event,object) in
// do something
}.disposed(by: self.tp_eventTokenBag)
複製程式碼
這裡我實現了一個小巧但比較全面的 EventBus 供參考:TPEventBus。
最後
我們可以看到,一對多場景中觀察者模式的應用流程都大同小異,但是如何更好用確是值得深思的。當然以上也只是我在一些使用場景上的思考,肯定會欠缺考慮,歡迎拍磚?。