Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save christopher-fuller/8eb9c7e134af432ad7a7878b106566c6 to your computer and use it in GitHub Desktop.
Save christopher-fuller/8eb9c7e134af432ad7a7878b106566c6 to your computer and use it in GitHub Desktop.
//
// NotificationObservingContext.swift
// Created by Christopher Fuller for Southern California Public Radio
// Original version at https://github.com/SCPR/swift-experiments/blob/master/source/NotificationObservable.swift
// Updated by Christopher Fuller for Swift 3 compatibility
//
// Copyright (c) 2016 Southern California Public Radio
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
protocol ObservableNotification {
static var name: Notification.Name { get }
init?()
}
extension ObservableNotification {
static var name: Notification.Name {
return Notification.Name(String(describing: self))
}
}
protocol UserInfoTransformableNotification: ObservableNotification {
var userInfo: [AnyHashable : Any]? { get }
init?(userInfo: [AnyHashable : Any]?)
}
extension UserInfoTransformableNotification {
init?() {
self.init()
}
}
extension NotificationCenter {
func post<T: ObservableNotification>(_ notification: T, object: AnyObject? = nil) {
let closure = {
self.post(name: type(of: notification).name, object: object)
}
Thread.isMainThread ? closure() : DispatchQueue.main.sync { closure() }
}
func post<T: UserInfoTransformableNotification>(_ notification: T, object: AnyObject? = nil) {
let closure = {
self.post(name: type(of: notification).name, object: object, userInfo: notification.userInfo)
}
Thread.isMainThread ? closure() : DispatchQueue.main.sync { closure() }
}
}
extension NotificationQueue {
func enqueue<T: ObservableNotification>(_ notification: T, object: AnyObject? = nil, postingStyle: NotificationQueue.PostingStyle, coalesceMask: NotificationQueue.NotificationCoalescing? = nil, dequeue: Bool = false) {
let closure = {
if dequeue {
self.dequeue(type(of: notification), object: object, coalesceMask: coalesceMask)
}
self.enqueue(
Notification(name: type(of: notification).name, object: object, userInfo: nil),
postingStyle: postingStyle,
coalesceMask: (coalesceMask ?? [ .onName, .onSender ]),
forModes: nil
)
}
Thread.isMainThread ? closure() : DispatchQueue.main.sync { closure() }
}
func enqueue<T: UserInfoTransformableNotification>(_ notification: T, object: AnyObject? = nil, postingStyle: NotificationQueue.PostingStyle, coalesceMask: NotificationQueue.NotificationCoalescing? = nil, dequeue: Bool = false) {
let closure = {
if dequeue {
self.dequeue(type(of: notification), object: object, coalesceMask: coalesceMask)
}
self.enqueue(
Notification(name: type(of: notification).name, object: object, userInfo: notification.userInfo),
postingStyle: postingStyle,
coalesceMask: (coalesceMask ?? [ .onName, .onSender ]),
forModes: nil
)
}
Thread.isMainThread ? closure() : DispatchQueue.main.sync { closure() }
}
func dequeue<T: ObservableNotification>(_ notifications: T.Type, object: AnyObject? = nil, coalesceMask: NotificationQueue.NotificationCoalescing? = nil) {
let closure = {
self.dequeueNotifications(
matching: Notification(name: notifications.name, object: object),
coalesceMask: Int((coalesceMask ?? [ .onName, .onSender ]).rawValue)
)
}
Thread.isMainThread ? closure() : DispatchQueue.main.sync { closure() }
}
}
class NotificationObserver {
private weak var observingContext: NotificationObservingContext!
fileprivate weak var object: AnyObject?
private let closure: NotificationObservingContext.NotificationPosted
fileprivate init(observingContext: NotificationObservingContext, object: AnyObject?, closure: @escaping NotificationObservingContext.NotificationPosted) {
self.observingContext = observingContext
self.object = object
self.closure = closure
}
@objc fileprivate func observe(notification: Notification) {
closure(self, notification)
}
func remove() {
observingContext.removeObserver(self)
}
}
class NotificationObservingContext {
typealias NotificationPosted = (_ observer: NotificationObserver, _ notification: Notification) -> Void
typealias ObservableNotificationPosted<T> = (_ observer: NotificationObserver, _ object: AnyObject?, _ notification: T) -> Void
fileprivate let notificationCenter: NotificationCenter
fileprivate var observers = [NotificationObserver]()
init(notificationCenter: NotificationCenter) {
self.notificationCenter = notificationCenter
}
convenience init() {
self.init(notificationCenter: NotificationCenter.default)
}
deinit {
removeObservers()
}
@discardableResult func observe(_ name: Notification.Name?, object: AnyObject? = nil, closure: @escaping NotificationPosted) -> NotificationObserver {
let observer = NotificationObserver(observingContext: self, object: object, closure: closure)
addObserver(observer, name: name, object: object)
return observer
}
@discardableResult func observe<T: ObservableNotification>(object: AnyObject? = nil, closure: @escaping ObservableNotificationPosted<T>) -> NotificationObserver {
let observer = NotificationObserver(observingContext: self, object: object) {
observer, notification in
let object = (notification.object as AnyObject?)
if let notification = T() {
closure(observer, object, notification)
}
}
addObserver(observer, name: T.self.name, object: object)
return observer
}
@discardableResult func observe<T: UserInfoTransformableNotification>(object: AnyObject? = nil, closure: @escaping ObservableNotificationPosted<T>) -> NotificationObserver {
let observer = NotificationObserver(observingContext: self, object: object) {
observer, notification in
let object = (notification.object as AnyObject?)
if let notification = T(userInfo: notification.userInfo) {
closure(observer, object, notification)
}
}
addObserver(observer, name: T.self.name, object: object)
return observer
}
func removeObservers(object: AnyObject) {
observers.filter({ $0.object === object }).forEach { removeObserver($0) }
}
func removeObservers() {
observers.forEach { removeObserver($0) }
}
}
private extension NotificationObservingContext {
func addObserver(_ observer: NotificationObserver, name: Notification.Name?, object: AnyObject?) {
observers.append(observer)
notificationCenter.addObserver(observer, selector: #selector(NotificationObserver.observe), name: name, object: object)
}
func removeObserver(_ observer: NotificationObserver) {
if let index = observers.index(where: { $0 === observer }) {
let observer = observers.remove(at: index)
notificationCenter.removeObserver(observer)
}
}
}
@christopher-fuller
Copy link
Author

Example Usage

class Example {

    private let context = NotificationObservingContext()

    init() {

        // Observe notification name:
        context.observe(.UIApplicationDidBecomeActive) {
            observer, notification in
            print(notification)
            // Only if you want a one time notification, you may remove the observer in the closure:
            observer.remove()
        }

        // If you want to remove an observer later, you may keep a reference to it:
        let observer = context.observe(.UIApplicationDidReceiveMemoryWarning) {
            observer, notification in
            print(notification)
        }

        // Arbitary example of removing that observer later:
        DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
            observer.remove()
        }

        // Observe custom notification:
        context.observe(object: self) {
            (observer, object, notification: ExampleNotification) in
            // Access custom notification properties:
            print(notification.dateA)
            print(notification.dateB)
        }

        let notification = ExampleNotification(dateA: .distantPast, dateB: .distantFuture)

        // Post custom notification:
        NotificationCenter.default.post(notification, object: self)

        // Enqueue custom notification:
        NotificationQueue.default.enqueue(notification, object: self, postingStyle: .asap)

        // If you want to remove all observers of a particular object later, you can do that too:
        DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
            [ weak self] in
            guard let _self = self else { return }
            _self.context.removeObservers(object: _self)
        }

    }

    deinit {
        // Nothing to do here since the removal of observers occurs automatically when context is deallocated.
    }

}

struct ExampleNotification: UserInfoTransformableNotification {

    private struct Key {
        static var dateA = "dateA"
        static var dateB = "dateB"
    }

    private(set) var dateA: Date
    private(set) var dateB: Date

    var userInfo: [AnyHashable: Any]? {
        return [
            Key.dateA: dateA,
            Key.dateB: dateB
        ]
    }

    init?(userInfo: [AnyHashable: Any]?) {
        guard
            let dateA = userInfo?[Key.dateA] as? Date,
            let dateB = userInfo?[Key.dateB] as? Date
            else {
                return nil
        }
        self.init(dateA: dateA, dateB: dateB)
    }

    init(dateA: Date, dateB: Date) {
        self.dateA = dateA
        self.dateB = dateB
    }

}

// If you do not need custom notification properties, use ObservableNotification instead:

struct AnotherExampleNotification: ObservableNotification {}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment