Skip to content

Instantly share code, notes, and snippets.

@tomlokhorst
Last active January 13, 2024 04:55
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save tomlokhorst/7fe49a03b8bac960eeaf2b991faa3680 to your computer and use it in GitHub Desktop.
//
// DarwinNotificationCenter.swift
//
// Created by Nonstrict on 2023-12-07.
//
import Foundation
import Combine
private let center = CFNotificationCenterGetDarwinNotifyCenter()
/// Wrapper around the application’s Darwin notification center from CFNotificationCenter.h
///
/// - Note: On macOS, consider using DistributedNotificationCenter instead
public final class DarwinNotificationCenter {
private init() {}
/// The application’s Darwin notification center.
public static var shared = DarwinNotificationCenter()
/// Posts a Darwin notification with the specified name.
public func post(name: String) {
CFNotificationCenterPostNotification(center, CFNotificationName(rawValue: name as CFString), nil, nil, true)
}
/// Registers an observer closure for Darwin notifications of the specified name.
///
/// Retain the returned `DarwinNotificationObservation` to keep the observer active.
///
/// Save the returned value in a variable, or store it in a bag.
///
/// ```
/// observation.store(in: &disposeBag)
/// ```
///
/// To stop observing the notifiation, deallocate the `DarwinNotificationObservation`, or call its `cancel()` method.
public func addObserver(name: String, callback: @escaping () -> Void) -> DarwinNotificationObservation {
let observation = DarwinNotificationObservation(callback: callback)
let pointer = UnsafeRawPointer(Unmanaged.passUnretained(observation.closure).toOpaque())
CFNotificationCenterAddObserver(center, pointer, notificationCallback, name as CFString, nil, .deliverImmediately)
return observation
}
}
private func notificationCallback(center: CFNotificationCenter?, observation: UnsafeMutableRawPointer?, name: CFNotificationName?, object _: UnsafeRawPointer?, userInfo _: CFDictionary?) {
guard let pointer = observation else { return }
let closure = Unmanaged<DarwinNotificationObservation.Closure>.fromOpaque(pointer).takeUnretainedValue()
closure.invoke()
}
/// Object that retains an observation of Darwin notifications.
///
/// Retain this object to keep the observer active.
///
/// Save this object in a variable, or store it in a bag.
///
/// ```
/// observation.store(in: &disposeBag)
/// ```
///
/// To stop observing the notifiation, deallocate the this object, or call the `cancel()` method.
public final class DarwinNotificationObservation: Cancellable {
// Wrapper class around the callback closure.
// This object can stay alive in the cancel block, after this Observation has been deallocated.
fileprivate class Closure {
let invoke: () -> Void
init(callback: @escaping () -> Void) {
self.invoke = callback
}
}
fileprivate let closure: Closure
fileprivate init(callback: @escaping () -> Void) {
self.closure = Closure(callback: callback)
}
deinit {
cancel()
}
/// Cancels the Darwin notification observation.
public func cancel() {
// Notifications are always delivered on the main thread.
// So we also remove the observer on the main thread,
// to make sure the closure object isn't deallocated during the execution of a notification.
DispatchQueue.main.async { [closure] in
let pointer = UnsafeRawPointer(Unmanaged.passUnretained(closure).toOpaque())
CFNotificationCenterRemoveObserver(center, pointer, nil, nil)
}
}
}
// MARK: - AsyncSequence
extension DarwinNotificationCenter {
/// Returns an asynchronous sequence of notifications for a given notification name.
func notifications(named name: String) -> AsyncStream<Void> {
AsyncStream { continuation in
let observation = addObserver(name: name) {
continuation.yield()
}
continuation.onTermination = { _ in
observation.cancel()
}
}
}
}
// MARK: - Combine
#if canImport(Combine)
extension DarwinNotificationCenter {
/// Returns a publisher that emits events when broadcasting notifications.
///
/// - Parameters:
/// - name: The name of the notification to publish.
/// - Returns: A publisher that emits events when broadcasting notifications.
public func publisher(for name: String) -> DarwinNotificationCenter.Publisher {
Publisher(center: self, name: name)
}
}
extension DarwinNotificationCenter {
/// A publisher that emits when broadcasting notifications.
public struct Publisher: Combine.Publisher {
public typealias Output = Void
public typealias Failure = Never
public let center: DarwinNotificationCenter
public let name: String
public init(center: DarwinNotificationCenter, name: String) {
self.center = center
self.name = name
}
public func receive<S>(subscriber: S) where S : Subscriber, S.Failure == Never, S.Input == Output {
let observation = center.addObserver(name: name) {
_ = subscriber.receive()
}
subscriber.receive(subscription: observation)
}
}
}
extension DarwinNotificationObservation: Subscription {
public func request(_ demand: Subscribers.Demand) {
}
}
#endif
//
// DarwinNotificationCenter.swift
//
// Created by Nonstrict on 2023-12-07.
//
import Foundation
import Combine
private let center = CFNotificationCenterGetDarwinNotifyCenter()
/// Wrapper around the application’s Darwin notification center from CFNotificationCenter.h
///
/// - Note: On macOS, consider using DistributedNotificationCenter instead
public final class DarwinNotificationCenter: NotificationCenter {
override private init() {}
fileprivate static var shared = DarwinNotificationCenter()
/// Posts a Darwin notification with the specified name.
public func post(name: Notification.Name) {
CFNotificationCenterPostNotification(center, CFNotificationName(rawValue: name as CFString), nil, nil, true)
}
@_documentation(visibility: private)
@available(*, deprecated, renamed: "post(name:)", message: "Darwin Notifications cannot send object or userInfo from Notification objects. Use `post(name:)` instead.")
override public func post(_ notification: Notification) {
post(name: notification.name)
}
@_documentation(visibility: private)
@available(*, deprecated, renamed: "post(name:)", message: "Darwin Notifications cannot send an object. Use `post(name:)` instead.")
override public func post(name aName: NSNotification.Name, object anObject: Any?) {
post(name: aName)
}
@_documentation(visibility: private)
@available(*, deprecated, renamed: "post(name:)", message: "Darwin Notifications cannot send object or userInfo. Use `post(name:)` instead.")
override public func post(name aName: NSNotification.Name, object anObject: Any?, userInfo aUserInfo: [AnyHashable : Any]? = nil) {
post(name: aName)
}
/// Registers an observer closure for Darwin notifications of the specified name.
///
/// Retain the returned `DarwinNotificationObservation` to keep the observer active.
///
/// Save the returned value in a variable, or store it in a bag.
///
/// ```
/// observation.store(in: &disposeBag)
/// ```
///
/// To stop observing the notifiation, deallocate the `DarwinNotificationObservation`, or call its `cancel()` method.
public func addObserver(name: Notification.Name, using block: @escaping () -> Void) -> DarwinNotificationObservation {
let observation = DarwinNotificationObservation(callback: block)
let pointer = UnsafeRawPointer(Unmanaged.passUnretained(observation.closure).toOpaque())
CFNotificationCenterAddObserver(center, pointer, notificationCallback, name as CFString, nil, .deliverImmediately)
return observation
}
@_documentation(visibility: private)
@available(*, deprecated, renamed: "addObserver(name:using:)", message: "Darwin Notifications cannot send or receive objects. Use addObserver(name:using:) instead.")
public override func addObserver(forName name: NSNotification.Name?, object obj: Any?, queue: OperationQueue?, using block: @escaping (Notification) -> Void) -> NSObjectProtocol {
guard let name else {
fatalError("Notification name cannot be nil for Darwin Notifications")
}
return addObserver(name: name) {
let notification = Notification(name: name)
if let queue {
queue.addOperation {
block(notification)
}
} else {
block(notification)
}
}
}
@_documentation(visibility: private)
@available(*, unavailable, message: "Darwin Notifications cannot send or receive objects. Use addObserver(name:using:) instead.")
public override func addObserver(_ observer: Any, selector aSelector: Selector, name aName: NSNotification.Name?, object anObject: Any?) {
fatalError("not implemented")
}
@_documentation(visibility: private)
@available(*, unavailable, message: "Not implememented for DarwinNotificationCenter. Use addObserver(name:using:) instead.")
public override class func addObserver(_ observer: NSObject, forKeyPath keyPath: String, options: NSKeyValueObservingOptions = [], context: UnsafeMutableRawPointer?) {
fatalError("not implemented")
}
@_documentation(visibility: private)
@available(*, unavailable, message: "Not implememented for DarwinNotificationCenter. Use addObserver(name:using:) instead.")
public override func addObserver(_ observer: NSObject, forKeyPath keyPath: String, options: NSKeyValueObservingOptions = [], context: UnsafeMutableRawPointer?) {
fatalError("not implemented")
}
}
extension NotificationCenter {
/// The application’s Darwin notification center.
public static var darwin: DarwinNotificationCenter = .shared
}
private func notificationCallback(center: CFNotificationCenter?, observation: UnsafeMutableRawPointer?, name: CFNotificationName?, object _: UnsafeRawPointer?, userInfo _: CFDictionary?) {
guard let pointer = observation else { return }
let closure = Unmanaged<DarwinNotificationObservation.Closure>.fromOpaque(pointer).takeUnretainedValue()
closure.invoke()
}
/// Object that retains an observation of Darwin notifications.
///
/// Retain this object to keep the observer active.
///
/// Save this object in a variable, or store it in a bag.
///
/// ```
/// observation.store(in: &disposeBag)
/// ```
///
/// To stop observing the notifiation, deallocate the this object, or call the `cancel()` method.
public final class DarwinNotificationObservation: NSObject, Cancellable {
// Wrapper class around the callback closure.
// This object can stay alive in the cancel block, after this Observation has been deallocated.
fileprivate class Closure {
let invoke: () -> Void
init(callback: @escaping () -> Void) {
self.invoke = callback
}
}
fileprivate let closure: Closure
fileprivate init(callback: @escaping () -> Void) {
self.closure = Closure(callback: callback)
}
deinit {
cancel()
}
/// Cancels the Darwin notification observation.
public func cancel() {
// Notifications are always delivered on the main thread.
// So we also remove the observer on the main thread,
// to make sure the closure object isn't deallocated during the execution of a notification.
DispatchQueue.main.async { [closure] in
let pointer = UnsafeRawPointer(Unmanaged.passUnretained(closure).toOpaque())
CFNotificationCenterRemoveObserver(center, pointer, nil, nil)
}
}
}
@tomlokhorst
Copy link
Author

Two alternative implementations for sending Darwin Notifications from Swift.
The first one is a standalone class, the second one is a NotificationCenter subclass.

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