Last active
May 9, 2025 13:20
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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