新项目采用了MVC架构, 在公司以往的几个项目采用的MVVM/MVVM-C架构下, 一些全局的数据同步都是使用的ReactiveCocoa/RxSwift中的可监听序列, 当然, 也不是说MVC用不了Functional reactive programming框架, 只是当前项目决定不用
鉴于此背景下, 新项目中全局的数据同步尝试使用系统的Notification, 有以下几个问题
- 对于Notification.Name, 有时根据业务需要自定义通知, 此时系统提供的无法满足需求
- 参数问题, 即Notification.userInfo, 如果想给通知追加参数, 系统是以字典的方式提供的, 这种方式不便于对发送和接收进行维护, 若是参数需要更改, 很有可能会漏掉某些地方, 而且编译器也是不会给你提供警告帮助的, 还有可能发送的参数改掉之后, 接收通知就无法正确解析了
- 在某个类中添加监听通知时, 需要在此类的的析构函数中去除此实例类, 这一点是非常容易忽略掉的
针对上面的问题, 分别进行一下封装
Notificaiton.Name
在Objective-C中, 是使用全局常量的方式构造一个通知名称对象, Swift提供了Extension的方式让用户进行自定义
1
2
3
4
5
6
extension Notification.Name {
static let CustomNotificationName: Notification.Name = Notification.Name(rawValue: "CustomNotificationNameRawValue")
}
// usage
NotificationCenter.default.addObserver(forName: .CustomNotificationName, object: nil, queue: nil, using: { _ in })
这样使用起来便方便了许多, 但是会有和系统通知名称重复的可能, 若是极致优化, 可以参考大神的建议
参数的封装
参考网络请求中, 对返回数据的反序列化, 我们可以创建自己的通知对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct CustomNotificationn {
let frame: CGRect
let duration: Double
}
extension CustomNotificationn {
init(note: Notification) {
frame = note.userInfo?["CustomNotification.Frame"] as? CGRect ?? .zero
duration = note.userInfo?["CustomNotification.Duration"] as? Double ?? .zero
}
func info() -> [AnyHashable : Any] {
return ["CustomNotification.Frame" : frame,
"CustomNotification.Duration" : duration]
}
}
数据的封装有了, 但是怎么使用呢, 直接的做法就是在监听回调中直接初始化实例对象然后使用, 但是还可以进一步封装, 让NotificationCenter直接发送和监听自定义通知对象, 达到使用者无感的地步
定义一个通知协议
1 2 3 4 5 6 7 8 9 10
protocol CustomNotificationProtocol { var name: Notification.Name { get } var objc: Any? { get } var userInfo: [AnyHashable : Any]? { get } } extension CustomNotificationProtocol { var objc: Any? { nil } var userInfo: [AnyHashable : Any]? { nil } }
让自定义通知对象遵守此协议
1 2 3 4 5 6 7 8
extension CustomNotificationn: CustomNotificationProtocol { var name: Notification.Name { .CustomNotificationName } var userInfo: [AnyHashable : Any]? { ["CustomNotification.Frame" : frame, "CustomNotification.Duration" : duration] } }
定义一个NotificationDescriptor用户桥接
1 2 3 4 5 6 7
struct NotificationDescriptor<A> { let name: Notification.Name let convert: (Notification) -> A } // usage let customNotificationDescriptor = NotificationDescriptor(name: .CustomNotificationName, convert: CustomNotificationn.init)
最后, 给NotificationCenter增加一个扩展, 这样, 就可以直接发送自定义通知对象, 并且监听时直接读取属性的方式拿到参数了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
extension NotificationCenter { @discardableResult func addObserver<A>(forDescriptor d: NotificationDescriptor<A>, object obj: Any, queue: OperationQueue?, using block: @escaping (A) -> ()) -> NSObjectProtocol { return addObserver(forName: d.name, object: nil, queue: nil, using: { note in block(d.convert(note)) }) } func post(note: CustomNotificationProtocol) { post(name: note.name, object: note.objc, userInfo: note.userInfo) } }
使用
1 2 3 4 5 6 7
let customNotificationDescriptor = NotificationDescriptor(name: .CustomNotificationName, convert: CustomNotificationn.init) NotificationCenter.default.addObserver(forDescriptor: customNotificationDescriptor) { descriptor in print(descriptor.frame, descriptor.duration) } NotificationCenter.default.post(note: CustomNotificationn(frame: .zero, duration: .zero))
监听者的移除
创建一个包含通知中心和监听类的类别, 使用deinit将观察者的生命周期与对象的生命周期联系起来
1
2
3
4
5
6
7
8
9
10
11
12
13
class Token {
let center: NotificationCenter
let observer: NSObjectProtocol
init(observer: NSObjectProtocol, center: NotificationCenter = .default) {
self.observer = observer
self.center = center
}
deinit {
center.removeObserver(observer)
}
}
优化NotificationCenter的扩展函数
1
2
3
4
5
6
7
8
9
10
11
extension NotificationCenter {
func addObserver<A>(forDescriptor d: NotificationDescriptor<A>,
object obj: Any? = nil,
queue: OperationQueue? = nil,
using block: @escaping (A) -> ()) -> Token? {
let observer = addObserver(forName: d.name, object: nil, queue: nil, using: { note in
block(d.convert(note))
})
return Token(observer: observer, center: self)
}
}
在启用监听的类中, 生命一个可选值的属性用来保存Token, 这样, 在这个类即将析构时, 也会调用token属性的deinit函数, 触发移除监听操作, 这种方式被称之为获取资源即初始化 虽然这种方式要求使用者要额外创建一个属性, 但是避免了使用者忘记移除监听者, 因为如果不接受addObserver返回值的话, 编译器会报警告来提示开发者 如果想提前结束监听, 也可以手动置nil来移除监听
1
2
3
4
5
6
7
var token: Token?
token = NotificationCenter.default.addObserver(forDescriptor: customNotificationDescriptor) { descriptor in
print(descriptor.frame, descriptor.duration)
}
token = nil