设计一个更加 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阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341