前言
Notification 作為蘋果開發(fā)平臺的通信方式, 雖然開銷比直接回調(diào)來的多, 但確實是在不引入第三方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 }
發(fā)送通知只需要:
NotificationCenter.default.post(name: Notification.Name("notification"), object: nil, userInfo: [:])
系統(tǒng)就會自動執(zhí)行注冊的回調(diào)
這個系統(tǒng)在 Objc 的時代其實沒什么問題, 畢竟 Objc 類型沒有嚴格限制, 但是放在 Swift 里就顯得格格不入了, 使用者第一次用或者忘記的時候都得去查文檔看 userInfo 里面有什么, 每次用都得浪費時間去試, 整個項目只用一次的東西可能沒什么關(guān)系, 但頻繁用的真的很煩
當然這套系統(tǒng)也有好處, 那就是泛用性特別好, 畢竟都使用了字典, 既不存在版本限制, 也不存在類型寫死, 甚至手動亂調(diào)用系統(tǒng)通知, 亂傳不是字典的類型都沒問題
那么, 怎么使用 Swift 強大的范型系統(tǒng)和方法重載來改造呢? 順便再改造一下系統(tǒng)自帶的通知.
設(shè)計
新的通知系統(tǒng)需要滿足以下幾點
- userInfo 類型必須是已知的, 如果是模型, 可能不存在的值定為可選就行, 方便調(diào)用者使用
- 為了簡化篇幅這里只實現(xiàn)帶閉包的addObserver, 當 addObserver 傳入 object 的時候, 回調(diào)里的 notification 就不需要帶 object 了, 有必要時手動把 object 帶進回調(diào)閉包就行
- 提供沒有 userInfo 版本的通知, 當初始化的通知不帶參數(shù)時, 去掉回調(diào)閉包的參數(shù) notification 比如:
addObserver(forName: Notification.Name("notification"), object: nil, queue: nil) { }
實現(xiàn)
初始化
基于上面三點易得一個區(qū)別于原版的 Notificatable:
struct Notificatable<Info> {
private init() { }
}
extension Notificatable {
static func name(_ name: String) -> ... {
...
}
}
初始化通知從:
let notification = Notification.Name("notification")
變?yōu)榱?
let notification = Notificatable<String>.name("notification")
為了實現(xiàn)沒有 userInfo 版本的通知, 引入一個 _Handler 作為實現(xiàn)載體, :
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)
}
}
創(chuàng)建的 notification 的類型也就變成
// Notificatable<String>._Handler<Any>
let notification = Notificatable<String>.name("notification")
引入 _Handler 后, 實現(xiàn)沒有 userInfo 版本的通知也就很簡單了:
extension Notificatable where Info == Never {
static func name(_ name: String) -> _Handler<Never> {
.init(name)
}
}
初始化:
// Notificatable<Never>._Handler<Never>
let notification = Notificatable.name("notification")
回調(diào)
addObserver 參考了一下 rx, 因為確實有些場景需要通知的回調(diào)一直存活的, 這種場景下直接使用原版就比較難用了, 這里簡單實現(xiàn)一個 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
根據(jù)設(shè)計, 這里根據(jù)綁不綁定 object 分為兩種 subscribe 方法, 綁定 object 的 subscribe 直接回調(diào) 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 的回調(diào)方法, 但由于不需要回調(diào) userInfo 了, 所以只需要直接把 Object 回調(diào)出去就行:
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)
}
}
發(fā)送
發(fā)送沒什么難的, 就兩套 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)
}
}
適配系統(tǒng)通知
改造回調(diào)方法
Notificatable._Handler
為 Notificatable._Handler 添加一個轉(zhuǎn)換 NSDictionary 為 Info 的方法數(shù)組和處理方法
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 轉(zhuǎn)換成 Notificatable
Swift 里不依賴第三方把 Dictionary 轉(zhuǎn)模型最直接的方法就是 Codable了, 但 userInfo 不是標準的 JSON 對象, 沒法直接使用系統(tǒng)的 JSONDecoder, 那么隨便自定義一個 Decoder 用于轉(zhuǎn)換 userInfo 不就好了嗎
不得不說每次寫 Decoder 的實現(xiàn)真的又臭又長, 80%的代碼都是重復的... 為了篇幅著想, 以下代碼不需要的部分用 fatalError() 略過, 錯誤處理也省略掉了, 除了枚舉外, 其他類型都不存在嵌套, 相關(guān)邏輯也省略掉了, 有興趣可以自己補充
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 實現(xiàn)一下轉(zhuǎn)換方法
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 的回調(diào)包含了: 鍵盤開始尺寸, 結(jié)束尺寸, 動畫時間等等, 非常適合作為例子
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 實現(xiàn)以下 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)
看一下效果, 雖然屬性名有點長, 但還是非常完美好用的
下一步
看到 notification.object 這個了沒有, 實際上大部分系統(tǒng)通知這個 object 都是 nil, 包括我們自己寫的通知大部分情況下都是沒有的, 有沒有辦法在聲明 Notificatable 的時候就過濾掉呢? 但是過濾掉這個又可能降低整體的拓展性, 對此各位是覺得有沒有必要呢? 歡迎在評論區(qū)留下看法
另外本文自己實現(xiàn)了一個簡單的 Disposable, 如果已經(jīng)集成了想 rx 之類的第三方, 可能會遇到 Object 類型不一樣的問題, 歡迎發(fā)表自己遇到的坑