设计一个更加 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 强大的范型系统和方法重载来改造呢? 顺便再改造一下系统自带的通知.
设计
新的通知系统需要满足以下几点
- userInfo 类型必须是已知的, 如果是模型, 可能不存在的值定为可选就行, 方便调用者使用
- 为了简化篇幅这里只实现带闭包的addObserver, 当 addObserver 传入 object 的时候, 回调里的 notification 就不需要带 object 了, 有必要时手动把 object 带进回调闭包就行
- 提供没有 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 类型不一样的问题, 欢迎发表自己遇到的坑