设计一个更加 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 {
    private init() { }
}
extension Notificatable {
    static func name(_ name: String) -> ... {
        ...
    }
}

初始化通知从:

let notification = Notification.Name("notification")

变为了:

let notification = Notificatable.name("notification")

为了实现没有 userInfo 版本的通知, 引入一个 _Handler 作为实现载体, :

struct Notificatable {
    private init() { }
    
    struct _Handler {
        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 {
        .init(name)
    }
}

创建的 notification 的类型也就变成

// Notificatable._Handler
let notification = Notificatable.name("notification")

引入 _Handler 后, 实现没有 userInfo 版本的通知也就很简单了:

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

初始化:

// Notificatable._Handler
let notification = Notificatable.name("notification")

回调

addObserver 参考了一下 rx, 因为确实有些场景需要通知的回调一直存活的, 这种场景下直接使用原版就比较难用了, 这里简单实现一个 Disposable:

private var disposeQueue = Set()
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(disposable: Notificatable.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(disposable: Notificatable.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 {
            let decoder: Decoder
        }
    }
}

extension Notificatable.Decoder: Swift.Decoder {
    func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer 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(_ 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(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer 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(_ 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._Handler {
        return .init(self)
    }
    
    func notificatable(userInfoType: Info.Type) -> Notificatable._Handler where Info: Decodable {
        var notification = Notificatable._Handler(self)
        notification.userInfoConverters.append {
            try? Info.init(from: Notificatable.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 系统)