設計一個更加 Swift 的 Notification 系統

前言

Notification 作為蘋果開發平臺的通信方式, 雖然開銷比直接回調來的多, 但確實是在不引入第三方SDK的前提下非常方便的方式, 使用方式也很簡單

注冊只需要:

NotificationCenter.default.addObserver(observer, selector: selector, name: Notification.Name("notification"), object: nil)

或者使用閉包的形式:

let obs = NotificationCenter.default.addObserver(forName: Notification.Name("notification"), object: nil, queue: nil) { (notification) in }

發送通知只需要:

NotificationCenter.default.post(name: Notification.Name("notification"), object: nil, userInfo: [:])

系統就會自動執行注冊的回調

這個系統在 Objc 的時代其實沒什么問題, 畢竟 Objc 類型沒有嚴格限制, 但是放在 Swift 里就顯得格格不入了, 使用者第一次用或者忘記的時候都得去查文檔看 userInfo 里面有什么, 每次用都得浪費時間去試, 整個項目只用一次的東西可能沒什么關系, 但頻繁用的真的很煩

當然這套系統也有好處, 那就是泛用性特別好, 畢竟都使用了字典, 既不存在版本限制, 也不存在類型寫死, 甚至手動亂調用系統通知, 亂傳不是字典的類型都沒問題

那么, 怎么使用 Swift 強大的范型系統和方法重載來改造呢? 順便再改造一下系統自帶的通知.

設計

新的通知系統需要滿足以下幾點

  1. userInfo 類型必須是已知的, 如果是模型, 可能不存在的值定為可選就行, 方便調用者使用
  2. 為了簡化篇幅這里只實現帶閉包的addObserver, 當 addObserver 傳入 object 的時候, 回調里的 notification 就不需要帶 object 了, 有必要時手動把 object 帶進回調閉包就行
  3. 提供沒有 userInfo 版本的通知, 當初始化的通知不帶參數時, 去掉回調閉包的參數 notification 比如: addObserver(forName: Notification.Name("notification"), object: nil, queue: nil) { }

實現

初始化

基于上面三點易得一個區別于原版的 Notificatable:

struct Notificatable<Info> {
    private init() { }
}
extension Notificatable {
    static func name(_ name: String) -> ... {
        ...
    }
}

初始化通知從:

let notification = Notification.Name("notification")

變為了:

let notification = Notificatable<String>.name("notification")

為了實現沒有 userInfo 版本的通知, 引入一個 _Handler 作為實現載體, :

struct Notificatable<Info> {
    private init() { }
    
    struct _Handler<Verify> {
        fileprivate var name: Foundation.Notification.Name
        fileprivate init(_ name: String) {
            self.name = .init(name)
        }
        fileprivate init(_ name: Foundation.Notification.Name) {
            self.name = name
        }
    }
}
extension Notificatable {
    static func name(_ name: String) -> _Handler<Any> {
        .init(name)
    }
}

創建的 notification 的類型也就變成

// Notificatable<String>._Handler<Any>
let notification = Notificatable<String>.name("notification")

引入 _Handler 后, 實現沒有 userInfo 版本的通知也就很簡單了:

extension Notificatable where Info == Never {
    static func name(_ name: String) -> _Handler<Never> {
        .init(name)
    }
}

初始化:

// Notificatable<Never>._Handler<Never>
let notification = Notificatable.name("notification")

回調

addObserver 參考了一下 rx, 因為確實有些場景需要通知的回調一直存活的, 這種場景下直接使用原版就比較難用了, 這里簡單實現一個 Disposable:

private var disposeQueue = Set<ObjectIdentifier>()
extension Notificatable {
    class Disposable {
        var holder: Any?
        init(_ holder: Any) {
            self.holder = holder
            disposeQueue.insert(.init(self))
        }
        deinit {
            holder = nil
        }
        func dispose() {
            disposeQueue.remove(.init(self))
        }
    }
}

為了簡化使用, 簡單模仿一下 rx 的 dispose(by: ), 順便給 NSObject 做分類方便接下來在 UIView/UIViewController 里直接用:

protocol NotificatableDisposeBy {
    func add<Info>(disposable: Notificatable<Info>.Disposable)
}

extension Notificatable.Disposable {
    func dispose(by owner: NotificatableDisposeBy) {
        owner.add(disposable: self)
        disposeQueue.remove(.init(self))
    }
}

extension NSObject: NotificatableDisposeBy {
    private struct AssociatedKey {
        static var queue = ""
    }
    private var notificatableDisposeQueue: [Any] {
        get {
            objc_getAssociatedObject(self, &AssociatedKey.queue) as? [Any] ?? []
        }
        set {
            objc_setAssociatedObject(self, &AssociatedKey.queue, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    func add<Info>(disposable: Notificatable<Info>.Disposable) {
        notificatableDisposeQueue.append(disposable)
    }
}

Notificatable._Handler

Verify == Any

根據設計, 這里根據綁不綁定 object 分為兩種 subscribe 方法, 綁定 object 的 subscribe 直接回調 Info 就行了

extension Notificatable._Handler where Verify == Any {
    struct Notification {
        let object: Any?
        let userInfo: Info
    }
    
    @discardableResult
    func subscribe(center: NotificationCenter = .default, queue: OperationQueue? = nil, _ action: @escaping (_ notification: Notification) -> Void) -> Notificatable.Disposable {
        let dispose =  center.addObserver(forName: name, object: nil, queue: queue) { noti in
            guard
                let info = noti.userInfo?[NotificatableUserInfoKey] as? Info
                else { return }
            
            action(.init(object: noti.object, userInfo: info))
        }
        return .init(dispose)
    }
    
    @discardableResult
    func subscribe(object: Any, center: NotificationCenter = .default, queue: OperationQueue? = nil, _ action: @escaping (_ info: Info) -> Void) -> Notificatable.Disposable {
        let dispose =  center.addObserver(forName: name, object: object, queue: queue) { noti in
            guard
                let info = noti.userInfo?[NotificatableUserInfoKey] as? Info
            else { return }
            
            action(info)
        }
        return .init(dispose)
    }
}

使用的時候:

notification.subscribe { (notification) in
  print("is (Notification) -> Void")
  print(notification)
}
    
notification.subscribe(object: NSObject()) { info in
  print("is (String) -> Void")
  print(info)
}

Verify == Never

同理不難得到 Verify == Never 的回調方法, 但由于不需要回調 userInfo 了, 所以只需要直接把 Object 回調出去就行:

extension Notificatable._Handler where Verify == Never {
    @discardableResult
    func subscribe(center: NotificationCenter = .default, queue: OperationQueue? = nil, _ action: @escaping (_ object: Any?) -> Void) -> Notificatable.Disposable {
        let dispose =  center.addObserver(forName: name, object: nil, queue: queue) { noti in
            action(noti.object)
        }
        return .init(dispose)
    }
    
    @discardableResult
    func subscribe(object: Any, center: NotificationCenter = .default, queue: OperationQueue? = nil, _ action: @escaping () -> Void) -> Notificatable.Disposable {
        let dispose =  center.addObserver(forName: name, object: object, queue: queue) { _ in
            action()
        }
        return .init(dispose)
    }
}

發送

發送沒什么難的, 就兩套 post 方法而已

Verify == Any

extension Notificatable._Handler where Verify == Any {
    func post(_ userInfo: Info, object: Any? = nil, center: NotificationCenter = .default) {
        center.post(name: name, object: object, userInfo: [
            NotificatableUserInfoKey: userInfo
        ])
    }
}

Verify == Never

extension Notificatable._Handler where Verify == Never {
    func post(object: Any? = nil, center: NotificationCenter = .default) {
        center.post(name: name, object: object, userInfo: nil)
    }
}

適配系統通知

改造回調方法

Notificatable._Handler

為 Notificatable._Handler 添加一個轉換 NSDictionary 為 Info 的方法數組和處理方法

fileprivate var userInfoConverters: [([AnyHashable: Any]) -> Info?] = [{
    $0[NotificatableUserInfoKey] as? Info
  }]
func convert(userInfo: [AnyHashable: Any]?) -> Info? {
  guard let userInfo = userInfo else { return nil }
  for converter in userInfoConverters {
    if let info = converter(userInfo) {
      return info
    }
  }
  return nil
}

subscribe

把 noti.userInfo?[NotificatableUserInfoKey] as? Info 改成了 convert(userInfo:), 例如:

@discardableResult
func subscribe(center: NotificationCenter = .default, queue: OperationQueue? = nil, _ action: @escaping (_ notification: Notification) -> Void) -> Notificatable.Disposable {
  let dispose =  center.addObserver(forName: name, object: nil, queue: queue) { noti in
    guard
      let info: Info = self.convert(userInfo: noti.userInfo)
    else { return }

    action(.init(object: noti.object, userInfo: info))
  }
  return .init(dispose)
}

把 Notification.Name 轉換成 Notificatable

Swift 里不依賴第三方把 Dictionary 轉模型最直接的方法就是 Codable了, 但 userInfo 不是標準的 JSON 對象, 沒法直接使用系統的 JSONDecoder, 那么隨便自定義一個 Decoder 用于轉換 userInfo 不就好了嗎

不得不說每次寫 Decoder 的實現真的又臭又長, 80%的代碼都是重復的... 為了篇幅著想, 以下代碼不需要的部分用 fatalError() 略過, 錯誤處理也省略掉了, 除了枚舉外, 其他類型都不存在嵌套, 相關邏輯也省略掉了, 有興趣可以自己補充

extension Notificatable {
    fileprivate class Decoder {
        var codingPath: [CodingKey] = []
        
        var userInfo: [CodingUserInfoKey: Any] = [:]
        
        var decodingUserInfo: [AnyHashable: Any]
        
        init(_ decodingUserInfo: [AnyHashable: Any]) {
            self.decodingUserInfo = decodingUserInfo
        }
        
        struct Container<Key: CodingKey> {
            let decoder: Decoder
        }
    }
}

extension Notificatable.Decoder: Swift.Decoder {
    func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key: CodingKey {
        .init(Container(decoder: self))
    }
    
    func unkeyedContainer() throws -> UnkeyedDecodingContainer {
        fatalError()
    }
    
    func singleValueContainer() throws -> SingleValueDecodingContainer {
        self
    }
}

extension Notificatable.Decoder.Container: KeyedDecodingContainerProtocol {
    
    var codingPath: [CodingKey] {
        decoder.codingPath
    }
    
    var allKeys: [Key] {
        decoder.decodingUserInfo.keys.compactMap {
            $0.base as? String }.compactMap { Key(stringValue: $0) }
    }
    
    func contains(_ key: Key) -> Bool {
        allKeys.contains {
            $0.stringValue == key.stringValue
        }
    }
    
    func decodeNil(forKey key: Key) throws -> Bool {
        let value = decoder.decodingUserInfo[key.stringValue]
        return value == nil || value is NSNull
    }
    
    func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool {
        decoder.decodingUserInfo[key.stringValue] as? Bool ?? false
    }
    
    func decode(_ type: String.Type, forKey key: Key) throws -> String {
        decoder.decodingUserInfo[key.stringValue] as? String ?? ""
    }
    
    func decode(_ type: Double.Type, forKey key: Key) throws -> Double {
        decoder.decodingUserInfo[key.stringValue] as? Double ?? 0
    }
    
    func decode(_ type: Float.Type, forKey key: Key) throws -> Float {
        decoder.decodingUserInfo[key.stringValue] as? Float ?? 0
    }
    
    func decode(_ type: Int.Type, forKey key: Key) throws -> Int {
        decoder.decodingUserInfo[key.stringValue] as? Int ?? 0
    }
    
    func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 {
        decoder.decodingUserInfo[key.stringValue] as? Int8 ?? 0
    }
    
    func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 {
        decoder.decodingUserInfo[key.stringValue] as? Int16 ?? 0
    }
    
    func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 {
        decoder.decodingUserInfo[key.stringValue] as? Int32 ?? 0
    }
    
    func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 {
        decoder.decodingUserInfo[key.stringValue] as? Int64 ?? 0
    }
    
    func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt {
        decoder.decodingUserInfo[key.stringValue] as? UInt ?? 0
    }
    
    func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 {
        decoder.decodingUserInfo[key.stringValue] as? UInt8 ?? 0
    }
    
    func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 {
        decoder.decodingUserInfo[key.stringValue] as? UInt16 ?? 0
    }
    
    func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 {
        decoder.decodingUserInfo[key.stringValue] as? UInt32 ?? 0
    }
    
    func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 {
        decoder.decodingUserInfo[key.stringValue] as? UInt64 ?? 0
    }
    
    func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
        guard let value = decoder.decodingUserInfo[key.stringValue] else {
            throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(key)."))
        }
        if let value = value as? T {
            return value
        } else {
            decoder.codingPath.append(key)
            defer {
                decoder.codingPath.removeLast()
            }
            return try T.init(from: decoder)
        }
    }
    
    func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
        fatalError()
    }
    
    func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
        fatalError()
    }
    
    func superDecoder() throws -> Decoder {
        fatalError()
    }
    
    func superDecoder(forKey key: Key) throws -> Decoder {
        fatalError()
    }
}

extension Notificatable.Decoder: SingleValueDecodingContainer {
    func decodeNil() -> Bool {
        let value = currentValue
        return value == nil || value is NSNull
    }
    var currentValue: Any? {
        decodingUserInfo[codingPath.last!.stringValue]
    }
    func decode(_ type: Bool.Type) throws -> Bool {
        currentValue as? Bool ?? false
    }
    
    func decode(_ type: String.Type) throws -> String {
        currentValue as? String ?? ""
    }
    
    func decode(_ type: Double.Type) throws -> Double {
        currentValue as? Double ?? 0
    }
    
    func decode(_ type: Float.Type) throws -> Float {
        currentValue as? Float ?? 0
    }
    
    func decode(_ type: Int.Type) throws -> Int {
        currentValue as? Int ?? 0
    }
    
    func decode(_ type: Int8.Type) throws -> Int8 {
        currentValue as? Int8 ?? 0
    }
    
    func decode(_ type: Int16.Type) throws -> Int16 {
        currentValue as? Int16 ?? 0
    }
    
    func decode(_ type: Int32.Type) throws -> Int32 {
        currentValue as? Int32 ?? 0
    }
    
    func decode(_ type: Int64.Type) throws -> Int64 {
        currentValue as? Int64 ?? 0
    }
    
    func decode(_ type: UInt.Type) throws -> UInt {
        currentValue as? UInt ?? 0
    }
    
    func decode(_ type: UInt8.Type) throws -> UInt8 {
        currentValue as? UInt8 ?? 0
    }
    
    func decode(_ type: UInt16.Type) throws -> UInt16 {
        currentValue as? UInt16 ?? 0
    }
    
    func decode(_ type: UInt32.Type) throws -> UInt32 {
        currentValue as? UInt32 ?? 0
    }
    
    func decode(_ type: UInt64.Type) throws -> UInt64 {
        currentValue as? UInt64 ?? 0
    }
    
    func decode<T>(_ type: T.Type) throws -> T where T : Decodable {
        guard let value = currentValue else {
            throw DecodingError.keyNotFound(codingPath.last!, DecodingError.Context(codingPath: self.codingPath, debugDescription: "No value associated with key \(codingPath.last!)."))
        }
        if let value = value as? T {
            return value
        } else {
            return try T.init(from: self)
        }
    }
}

給 Notification.Name 實現一下轉換方法

extension Notification.Name {
    func notificatable() -> Notificatable<Never>._Handler<Never> {
        return .init(self)
    }
    
    func notificatable<Info>(userInfoType: Info.Type) -> Notificatable<Info>._Handler<Any> where Info: Decodable {
        var notification = Notificatable<Info>._Handler<Any>(self)
        notification.userInfoConverters.append {
            try? Info.init(from: Notificatable<Info>.Decoder($0))
        }
        return notification
    }
}

完成了!

測試

讓我們拿 UIResponder.keyboardWillChangeFrameNotification 試一下, keyboardWillChangeFrameNotification 的回調包含了: 鍵盤開始尺寸, 結束尺寸, 動畫時間等等, 非常適合作為例子

struct KeyboardWillChangeFrameInfo: Decodable {
    let UIKeyboardCenterBeginUserInfoKey: CGPoint
    let UIKeyboardCenterEndUserInfoKey: CGPoint
    
    let UIKeyboardFrameBeginUserInfoKey: CGRect
    let UIKeyboardFrameEndUserInfoKey: CGRect
    
    let UIKeyboardIsLocalUserInfoKey: Bool
    
    let UIKeyboardAnimationDurationUserInfoKey: TimeInterval
    let UIKeyboardAnimationCurveUserInfoKey: UIView.AnimationOptions
}

不要忘記也給 UIView.AnimationOptions 實現以下 Decoable

extension UIView.AnimationOptions: Decodable {
    public init(from decoder: Decoder) throws {
        try self.init(rawValue: decoder.singleValueContainer().decode(UInt.self))
    }
}

找個有輸入框的 viewController 試一下

let notification = UIResponder.keyboardWillChangeFrameNotification.notificatable(userInfoType: KeyboardWillChangeFrameInfo.self)

notification.subscribe { (notification) in
    print(notification.userInfo.UIKeyboardFrameEndUserInfoKey)
}.dispose(by: self)

看一下效果, 雖然屬性名有點長, 但還是非常完美好用的

image-20201027113428492.png

下一步

看到 notification.object 這個了沒有, 實際上大部分系統通知這個 object 都是 nil, 包括我們自己寫的通知大部分情況下都是沒有的, 有沒有辦法在聲明 Notificatable 的時候就過濾掉呢? 但是過濾掉這個又可能降低整體的拓展性, 對此各位是覺得有沒有必要呢? 歡迎在評論區留下看法

另外本文自己實現了一個簡單的 Disposable, 如果已經集成了想 rx 之類的第三方, 可能會遇到 Object 類型不一樣的問題, 歡迎發表自己遇到的坑

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379