美文网首页
设计一个更加 Swift 的 Notification 系统

设计一个更加 Swift 的 Notification 系统

作者: 庄msia | 来源:发表于2020-10-27 15:26 被阅读0次

    前言

    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 类型不一样的问题, 欢迎发表自己遇到的坑

    相关文章

      网友评论

          本文标题:设计一个更加 Swift 的 Notification 系统

          本文链接:https://www.haomeiwen.com/subject/yiaimktx.html