Skip to content

Instantly share code, notes, and snippets.

@Amzd
Last active May 9, 2025 13:20
Show Gist options
  • Save Amzd/b15d27bb73f5c4f3126c5413f5fcc985 to your computer and use it in GitHub Desktop.
Save Amzd/b15d27bb73f5c4f3126c5413f5fcc985 to your computer and use it in GitHub Desktop.
Swift DarwinNotificationCenter using CFNotificationCenterGetDarwinNotifyCenter to allow checking wether the main app is running from an extension. Note that this kinda sucks as CFNotificationCenter does not support multi threading and will always send/receive on the main thread so if your main thread is blocked this will also be blocked.
@objc public enum DarwinNotification: Int {
case appRunningQuestion
case appRunningConfirmation
var name: CFString {
switch self {
case .appRunningQuestion: "com.example.app_running_question" as CFString
case .appRunningConfirmation: "com.example.app_running_confirmation" as CFString
}
}
init?(name: CFString) {
switch name {
case Self.appRunningQuestion.name: self = .appRunningQuestion
case Self.appRunningConfirmation.name: self = .appRunningConfirmation
default: return nil
}
}
}
public class DarwinNotificationCenter {
public static var current = DarwinNotificationCenter()
private init() {}
private var dispatchTable: [DarwinNotification: [ObjectIdentifier: (DarwinNotification) -> Void]] = [:]
private var actingObservers: [ObjectIdentifier: DarwinNotificationCenterActingObserver] = [:]
/// Adds an entry to the notification center to receive notifications that passed to the provided block.
///
/// - Parameters:
/// - notification: The notification to register for delivery to the observer.
/// - callback:
/// The closure that executes when receiving a notification.
/// The notification center copies the closure. The notification center strongly holds the copied closure until you remove the observer registration.
/// The closure takes one argument: the notification.
/// - Returns: An opaque object to act as the observer. Notification center strongly holds this return value until you remove the observer registration.
public func addObserver(for notification: DarwinNotification, using callback: @escaping (DarwinNotification) -> Void) -> AnyObject {
let actingObserver = DarwinNotificationCenterActingObserver(callback: callback)
actingObservers[ObjectIdentifier(actingObserver)] = actingObserver
addObserver(actingObserver, selector: #selector(DarwinNotificationCenterActingObserver.callback), for: notification)
return actingObserver
}
/// Adds an entry to the notification center to call the provided selector with the notification.
///
/// - Parameters:
/// - observer: An object to register as an observer.
/// - selector: A selector that specifies the message the receiver sends observer to alert it to the notification posting. The method that selector specifies must have one and only one argument (an instance of DarwinNotification).
/// - notification: The notification to register for delivery to the observer.
public func addObserver(_ observer: AnyObject, selector: Selector, for notification: DarwinNotification) {
// Start observing if this is the first observer with this notification name
if dispatchTable[notification, default: [:]].isEmpty {
CFNotificationCenterAddObserver(
CFNotificationCenterGetDarwinNotifyCenter(),
Unmanaged.passUnretained(self).toOpaque(),
{ _, _, name, _, _ in
guard let name, let notification = DarwinNotification(name: name.rawValue) else { return }
DarwinNotificationCenter.current.dispatchTable[notification]?.values.forEach { $0(notification) }
},
notification.name,
nil, // ignored
.deliverImmediately // ignored
)
}
// Save observer
let id = ObjectIdentifier(observer)
dispatchTable[notification, default: [:]][id] = { [weak observer] notification in
if let observer {
_ = observer.perform(selector, with: notification)
} else { // observer has been deallocated so remove it
Self.current.removeObserver(id: id, name: notification)
}
}
}
/// Removes all entries specifying an observer from the notification center�s dispatch table.
public func removeObserver(_ observer: AnyObject) {
let id = ObjectIdentifier(observer)
dispatchTable.filter { $0.value[id] != nil }.keys.forEach { subscribedNotification in
removeObserver(id: id, name: subscribedNotification)
}
}
/// Removes matching entries from the notification center�s dispatch table.
public func removeObserver(_ observer: AnyObject, name notification: DarwinNotification) {
removeObserver(id: ObjectIdentifier(observer), name: notification)
}
private func removeObserver(id: ObjectIdentifier, name notification: DarwinNotification) {
// prevent leak in the case where user might strongly reference the acting observer inside the callback
actingObservers[id]?._callback = nil
actingObservers[id] = nil
dispatchTable[notification]?[id] = nil
if dispatchTable[notification, default: [:]].isEmpty {
CFNotificationCenterRemoveObserver(
CFNotificationCenterGetDarwinNotifyCenter(),
Unmanaged.passUnretained(self).toOpaque(),
CFNotificationName(notification.name),
nil // ignored
)
}
}
/// Posts a given notification to the notification center.
public func post(_ notification: DarwinNotification) {
CFNotificationCenterPostNotification(
CFNotificationCenterGetDarwinNotifyCenter(),
CFNotificationName(notification.name),
nil,
nil,
true
)
}
}
extension DarwinNotificationCenter {
/// - note: A reply can take quite a while if the main thread is super busy but
/// CFNotificationCenterAddObserver only supports main thread.
public func didReply(_ reply: DarwinNotification, to: DarwinNotification, timeout: DispatchTime) -> Bool {
let group = DispatchGroup()
group.enter()
let observer = addObserver(for: reply) { _ in group.leave() }
post(to)
let result = group.wait(timeout: timeout)
removeObserver(observer)
return result == .success
}
}
/// An Acting Observer for subscribing to darwin notifications using a closure
private class DarwinNotificationCenterActingObserver {
var _callback: ((DarwinNotification) -> Void)?
init(callback: @escaping (DarwinNotification) -> Void) {
self._callback = callback
}
@objc func callback(notification: DarwinNotification) {
_callback?(notification)
}
}
class AppDelegate \*...*\ {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
\*...*\
DarwinNotificationCenter.current.addObserver(self, selector: #selector(Self.appRunningQuestion), for: .appRunningQuestion)
\*...*\
}
/// Other processes like the Notification Service Extension can post
/// DarwinNotification.appRunningQuestion in which case we reply with .appRunningConfirmation
@objc func appRunningQuestion(notification: DarwinNotification) {
DarwinNotificationCenter.current.post(.appRunningConfirmation)
}
}
// in an extension (eg Notification Service Extension)
if DarwinNotificationCenter.current.didReply(.appRunningConfirmation, to: .appRunningQuestion, timeout: .now() + .seconds(1)) {
// app is running
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment