Swift 实现 iOS 类型安全的 Notification

iOS中Notification (或者NSNotification)是一对多通信的常用手段。但是开发者们长期以来对Notification的诟病不断,一不小心,整个app就会变得notification满天飞,加上它本身是基于String和Dictionary作为信息载体的,使得它难以被管理。

Swift中的Notification虽然增加了Notification.Name,使其有了一定类型安全性,但对它的信息载荷userInfo依然使用了任意类型的Dictionary,使其在解析上很难有类型安全的保证。本文将介绍一种方法,使其在通知名称和信息载荷上都保证类型安全,但又不过多更改Notification的使用方法。

首先我们创建一个protocol来约束Notification.NameuserInfo的关系。

protocol NotificationType {
    associatedtype UserInfoType
    
    static var name: Notification.Name { get }
    var userInfo: UserInfoType { set get }
    
    init(userInfo: UserInfoType)
}

以上的代码,我们使用了associatedtype使这个protocol范型化,而UserInfoType便是强类型userInfo的类型。这个类型可以在定义实际的Notification时定义。

接下来,我们对这个NotificationType进行扩展,使其使用起来更简单。

extension NotificationType {
    static var name: Notification.Name {
        return Notification.Name("ProjectName.Notification.\(self)")
    }
    
    /// Post a notification with the poster
    ///
    /// - Parameter object: The object posting the notification.
    /// - Parameter notificationCenter: The notification center, if the value is nil, the default notification center will be used.
    func post(by object: Any?, via notificationCenter: NotificationCenter? = nil) {
        let center = notificationCenter ?? NotificationCenter.default
        let userInfoDict = ["userInfo": userInfo]
        center.post(name: Self.name, object: object, userInfo: userInfoDict)
    }
}

以上的扩展,先是定义了默认的Notification.Name生成的方法,即通过NotificationType的名称来推导。下面的post方法是用来规范notification发送的结构。从代码中我们能看出来,发送时强类型的信息载荷对象被装到一个字典里,并强制使用"userInfo"作为key,这样在解析的时候我们可以直接从userInfo里拿出强类型的object了。

有了这两段代码,我们便可以通过下面的方法定义和发送Notification了:

有载荷的Notificaiton:

struct UserCreated: NotificationType {
    struct UserInfo {
        var userId: String
        var userName: String
    }
    
    var userInfo: UserInfo
}

这里,我们创建了UserCreated,它实现了NotificationType。我们在struct中定义了另一个struct UserInfo来作为其载荷类型(这样做使得UserInfo有了namespace,避免冲突),并且在下面显式指定userInfo的类型为UserInfo。这时,Swift的类型推断能够正确识别associatedtype为UserCreated.UserInfo。这样这个Notification就被构造好了。

我们要发送UserCreated很简单,只需要

UserCreated(userInfo: UserCreated.UserInfo(userId: "123", userName: "abc")).post(by: nil)

接下来我们要解决接收的问题了。我们要充分利用之前在实现protocol时的约定,使得解析userInfo的内容简化。我们通过扩展NotificationType的方式,使其可以辅助userInfo的解析,并且扩展NotificationCenter方法,使得接收部分变得强类型。

extension NotificationType where UserInfoType: Any {
    
    /// Get user info with strong type
    static func parse(notification: Notification)-> UserInfoType {
        return notification.userInfo!["userInfo"] as! UserInfoType
    }
}

extension NotificationCenter {
    @discardableResult
    func addObserver(for type: T.Type,
                                          object: Any?,
                                          queue: OperationQueue?,
                                          using block: @escaping ((Notification, T.UserInfoType)-> Void)) -> NSObjectProtocol {
        return self.addObserver(forName: T.name, object: object, queue: queue) { (notification) in
            let userInfo = T.parse(notification: notification)
            block(notification, userInfo)
        }
    }
}

有了上面的扩展,我们就可以将接收部分写成:

NotificationCenter.default.addObserver(for: UserCreated.self, object: nil, queue: nil) { (_, userInfo) in
    print(userInfo.userId)
    print(userInfo.userName)
}

这样以来,notification在发送和接收过程中都保持了类型安全。

另外,我们可以增加一个扩展,使得无载荷的notification变得更简单:

extension NotificationType where UserInfoType == Void {
    /// for whose user info type is void, make a covenient initializer
    init() {
        self.init(userInfo: ())
    }
}

这样,无载荷的notification使用就变成

SomeOperationCompleted().post(by: nil)

另外,如果你想对Notification.Name定制,可以在实现NotificationType时覆盖默认生成方法:

struct NameOverrided: NotificationType {
    static var name: Notification.Name = Notification.Name(rawValue: "Something else")
    var userInfo: Void
}

完整的例子

import UIKit

protocol NotificationType {
    associatedtype UserInfoType
    
    static var name: Notification.Name { get }
    var userInfo: UserInfoType { set get }
    
    init(userInfo: UserInfoType)
}

extension NotificationType {
    static var name: Notification.Name {
        return Notification.Name("Stowed.Notification.\(self)")
    }
    
    /// Post a notification with the poster
    ///
    /// - Parameter object: The object posting the notification.
    /// - Parameter notificationCenter: The notification center, if the value is nil, the default notification center will be used.
    func post(by object: Any?, via notificationCenter: NotificationCenter? = nil) {
        let center = notificationCenter ?? NotificationCenter.default
        let userInfoDict = ["userInfo": userInfo]
        center.post(name: Self.name, object: object, userInfo: userInfoDict)
    }
}

extension NotificationType where UserInfoType == Void {
    /// for whose user info type is void, make a covenient initializer
    init() {
        self.init(userInfo: ())
    }
}

extension NotificationType where UserInfoType: Any {
    
    /// Get user info with strong type
    static func parse(notification: Notification)-> UserInfoType {
        return notification.userInfo!["userInfo"] as! UserInfoType
    }
}

extension NotificationCenter {
    @discardableResult
    func addObserver(for type: T.Type,
                                          object: Any?,
                                          queue: OperationQueue?,
                                          using block: @escaping ((Notification, T.UserInfoType)-> Void)) -> NSObjectProtocol {
        return self.addObserver(forName: T.name, object: object, queue: queue) { (notification) in
            let userInfo = T.parse(notification: notification)
            block(notification, userInfo)
        }
    }
}

struct UserCreated: NotificationType {
    struct UserInfo {
        var userId: String
        var userName: String
    }
    
    var userInfo: UserInfo
}

struct SomeOperationCompleted: NotificationType {
    var userInfo: Void
}

struct NameOverrided: NotificationType {
    static var name: Notification.Name = Notification.Name(rawValue: "Something else")
    var userInfo: Void
}



NotificationCenter.default.addObserver(for: UserCreated.self, object: nil, queue: nil) { (_, userInfo) in
    print(userInfo.userId)
    print(userInfo.userName)
}

NotificationCenter.default.addObserver(for: SomeOperationCompleted.self, object: nil, queue: nil) { (_, _) in
    print("Something completed")
}

UserCreated(userInfo: UserCreated.UserInfo(userId: "123", userName: "abc")).post(by: nil)

SomeOperationCompleted().post(by: nil)

你可能感兴趣的:(Swift 实现 iOS 类型安全的 Notification)