Skip to content

Instantly share code, notes, and snippets.

@tomlokhorst
Last active July 12, 2024 12:42
Show Gist options
  • Save tomlokhorst/7fe49a03b8bac960eeaf2b991faa3680 to your computer and use it in GitHub Desktop.
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

Yes, notifications can only be received if the process is running.
If you want a full featured communication system between two processes, you can do things like values to a shared file or user defaults (in an App Group), and send notifications to the other process to check the file.
This way, if one of the processes is not running, it can still check the shared file when it starts back up. There's several libraries that implement this, I haven't used them, so I don't have a recommentation.

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