Skip to content

Instantly share code, notes, and snippets.

@tomlokhorst
Last active October 16, 2024 10:14
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

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

@letsbondiway
Copy link

@tomlokhorst Are apps using Darwin Notifications not rejected by Apple during review? Someone mentions here so - https://developer.apple.com/forums/thread/7411

@tomlokhorst
Copy link
Author

tomlokhorst commented Jun 27, 2024

Yes, listening to Apples internal notifications seems to be qualified as using a “private API”, and can (will?) get your app rejected.

But using Darwin Notifications for posting and listening to your own notifications is fine. The CFNotificationCenter APIs are public and documented, so you can use those.

@letsbondiway
Copy link

Ok, got it. Thanks!

@letsbondiway
Copy link

@tomlokhorst I have another question/scenario on which I wanted your thoughts on. I have an app and and an extension of the app. I post for a Darwin notification from the extension which is observed in the app. What I have seen is that the app receives the notification only when it is still in the background state. If the app is in terminated state, say if I manually remove the app from background apps list, it does not receive the notification. I guess that is expected?

@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