-
-
Save AvdLee/07de0b0fe7dbc351541ab817b9eb6c1c to your computer and use it in GitHub Desktop.
// | |
// DarwinNotificationCenter.swift | |
// | |
// Copyright © 2017 WeTransfer. All rights reserved. | |
// | |
import Foundation | |
/// A Darwin notification payload. It does not contain any userInfo, a Darwin notification is purely event handling. | |
public struct DarwinNotification { | |
/// The Darwin notification name | |
public struct Name: Equatable { | |
/// The CFNotificationName's value | |
fileprivate var rawValue: CFString | |
} | |
/// The Darwin notification name | |
var name: Name | |
/// Initializes the notification based on the name. | |
fileprivate init(_ name: Name) { | |
self.name = name | |
} | |
} | |
// MARK: - | |
extension DarwinNotification.Name { | |
/// Initializes a new Notification Name, based on a custom string. This string should be identifying for not only this notification, but for the full system. Therefore, you should include a bundle identifier to the string. | |
public init(_ rawValue: String) { | |
self.rawValue = rawValue as CFString | |
} | |
/// Initialize a new Notification Name, based on a CFNotificationName. | |
init(_ cfNotificationName: CFNotificationName) { | |
rawValue = cfNotificationName.rawValue | |
} | |
public static func == (lhs: DarwinNotification.Name, rhs: DarwinNotification.Name) -> Bool { | |
return (lhs.rawValue as String) == (rhs.rawValue as String) | |
} | |
} | |
// MARK: - | |
/// A system-wide notification center. This means that all notifications will be delivered to all interested observers, regardless of the process owner. Darwin notifications don't support userInfo payloads to the notifications. This wrapper is thread-safe. | |
public final class DarwinNotificationCenter { | |
/// An active observation by an observer. | |
fileprivate final class Observation { | |
/// The handler to be executed when the notification is received. | |
let handler: NotificationHandler | |
/// The notification name where the observer is interested in. | |
let name: DarwinNotification.Name | |
/// The interested object | |
weak var observer: AnyObject? | |
init(observer: AnyObject, name: DarwinNotification.Name, handler: @escaping NotificationHandler) { | |
self.observer = observer | |
self.name = name | |
self.handler = handler | |
observe() | |
} | |
} | |
/// The handler type to be executed when the notification is received. | |
public typealias NotificationHandler = ((DarwinNotification) -> Void) | |
/// The shared DarwinNotificationCenter, it will always return the same instance. | |
public static var shared = DarwinNotificationCenter() | |
/// The underlying CFNotificationCenter. | |
private let center = CFNotificationCenterGetDarwinNotifyCenter() | |
/// All observation info. This frequently needs some cleanup, as done by the cleanupObservers() method. | |
private var observations = [Observation]() | |
/// A serial queue to sync all observation changes onto, to make the wrapper thread-safe. | |
private let queue = DispatchQueue(label: "com.wetransfer.darwin-notificationcenter", qos: .default, attributes: [], autoreleaseFrequency: .workItem) | |
private init() {} | |
// MARK: - | |
/// Cleanup all deallocated observers | |
private func cleanupObservers() { | |
queue.async { | |
self.observations = self.observations.filter { (observation) -> Bool in | |
let stillAlive = observation.observer != nil | |
if !stillAlive { | |
observation.unobserve() | |
} | |
return stillAlive | |
} | |
} | |
} | |
/// Adds a given observer, to watch for the given Darwin notification, using a given handler. | |
/// | |
/// - Parameters: | |
/// - observer: The observer that is interested in the notification. Whenever the observer gets deallocated, the handler won't be guaranteed to be called anymore. | |
/// - name: The notification name of interest. | |
/// - handler: The handler to be executed when the notification is received. This will always be executed on a dedicated userinteractive queue, so NOT the main queue. If you want, you can dispatch to the main queue yourself. | |
public func addObserver(_ observer: AnyObject, for name: DarwinNotification.Name, using handler: @escaping NotificationHandler) { | |
cleanupObservers() | |
queue.async { | |
let observation = Observation(observer: observer, name: name, handler: handler) | |
if !self.observations.contains(observation) { | |
self.observations.append(observation) | |
} | |
} | |
} | |
/// Remove a given observer. By default, all notifications for the given observer will be removed, but it's also possible to pass a specific notification name. | |
/// | |
/// - Parameters: | |
/// - observer: The observer that needs to be removed. | |
/// - name: The notification name that is not interesting anymore. This is nil by default, meaning that all notifications will be removed for the given observer. | |
public func removeObserver(_ observer: AnyObject, for name: DarwinNotification.Name? = nil) { | |
cleanupObservers() | |
queue.async { | |
self.observations = self.observations.filter { (observation) -> Bool in | |
let shouldRetain = observer !== observation.observer || (name != nil && observation.name != name) | |
if !shouldRetain { | |
observation.unobserve() | |
} | |
return shouldRetain | |
} | |
} | |
} | |
/// Checks whether the given object is an observer for the given notification name. | |
/// | |
/// - Parameters: | |
/// - observer: The observer to check. | |
/// - name: The name to check. | |
/// - Returns: Whether the object is an observer. | |
public func isObserver(_ observer: AnyObject, for name: DarwinNotification.Name? = nil) -> Bool { | |
cleanupObservers() | |
return queue.sync(execute: { () -> Bool in | |
return observations.contains(where: { (observation) -> Bool in | |
return observer === observation.observer && (name == nil || observation.name == name) | |
}) | |
}) | |
} | |
/// Posts the given Notification name to the system. | |
/// | |
/// - Parameter name: The notification name to post. | |
public func postNotification(_ name: DarwinNotification.Name) { | |
// Before posting a notification, cleanup all observers that are deallocated. | |
cleanupObservers() | |
guard let cfNotificationCenter = self.center else { | |
fatalError("Invalid CFNotificationCenter") | |
} | |
CFNotificationCenterPostNotification(cfNotificationCenter, CFNotificationName(rawValue: name.rawValue), nil, nil, false) | |
} | |
/// Execute the observation handler for all observers that observe the given notification name. | |
private func signalNotification(_ name: DarwinNotification.Name) { | |
cleanupObservers() | |
queue.async { | |
let affectedObservations = self.observations.filter({ (observation) -> Bool in | |
return observation.name == name | |
}) | |
let notification = DarwinNotification(name) | |
for observation in affectedObservations { | |
observation.handler(notification) | |
} | |
} | |
} | |
} | |
// MARK: - | |
extension DarwinNotificationCenter.Observation: Equatable { | |
/// Start observing the notification. | |
fileprivate func observe() { | |
guard let cfCenter = DarwinNotificationCenter.shared.center else { | |
fatalError("Invalid Darwin observation info.") | |
} | |
// A notification callback. Since this is a C function pointer, it can not have any ownership context. | |
let callback: CFNotificationCallback = { (center, observer, name, object, userInfo) in | |
guard let cfName = name else { | |
return | |
} | |
let notificationName = DarwinNotification.Name(cfName) | |
DarwinNotificationCenter.shared.signalNotification(notificationName) | |
} | |
let observer = Unmanaged.passUnretained(self).toOpaque() | |
CFNotificationCenterAddObserver(cfCenter, observer, callback, name.rawValue, nil, .coalesce) | |
} | |
/// Stop observing the notification. This should be done whenever the observation is going to be removed. | |
fileprivate func unobserve() { | |
guard let cfCenter = DarwinNotificationCenter.shared.center else { | |
fatalError("Invalid Darwin observation info.") | |
} | |
let notificationName = CFNotificationName(rawValue: name.rawValue) | |
var observer = self | |
CFNotificationCenterRemoveObserver(cfCenter, &observer, notificationName, nil) | |
} | |
static func == (lhs: DarwinNotificationCenter.Observation, rhs: DarwinNotificationCenter.Observation) -> Bool { | |
return lhs.observer === rhs.observer && lhs.name == rhs.name | |
} | |
} |
It shouldn't as we check whether an observation is already registered. Could you point me more precisely where you think that is caused?
If you add two observers (different instances, lets say a view and a class for caching data) for the same notificationname, then the generated observations will hold two different observers and the equality-check will yield false -> you have two observations on the same name -> two callbacks each time CFNotificationCenter notifies for that name. If im reasoning correctly that is..
Sorry for the wall of text, answering from the phone :)
No worries, it made me understand it! We shouldn't add another observer here: https://gist.github.com/AvdLee/07de0b0fe7dbc351541ab817b9eb6c1c#file-darwinnotificationcenter-swift-L207 if it's already observing a certain name.
I don't have the time to solve it right now but feel free to go ahead and let me know what's needed to change it!
Yup, correct. I would probably change the Observation
class itself, make it hold an array of tuple's which binds observers and their callbacks together. E.g. observers = [(WeakObserver, NotificationHandler)]
. Where WeakObserver would be a class that holds an AnyObject weakly. Then you would have to change a bit on isObserver, addObserver, removeObserver, cleanObservers and signalNotification. Then you would only have one Observation
per DarwinNotification.
There is a variant where one can wrap the notificationhandler closure and before calling it, checking if the observer is nil and autoremove itself. It's quite nice imo (but depends on how often one wants to remove observers):
func add(observer: AnyObject, handler: Handler) {
let id = ObjectIdentifier(observer)
let callback = { [weak observer, weak self] (name) in
if (observer == nil) {
self?.callbacks[id] = nil
return
}
handler(name)
}
callbacks[id] = callback
}
Using a dictionary in this case, which makes it easier to remove them with the ObjectIdentifier.
There is probably more ways to achieve this, maybe some which requires less rewrites..
Hello @AvdLee,
Do you know, does Darwin notifications mechanism is still viable approach for communication between the container app and extension on iOS? I've seen some controversial information on web, particularly there are some unresolved issues on MMWormhole project, which is used the same approach. There are reports that starting from iOS 13 this mechanism had been stopped working for container app <-> extension.
Any information would be much appreciated.
With regards.
@OleksandStepanov we're no longer using it and replaced it with Persistent History Tracking.
@OleksandStepanov it is possible to use an appgroup and a userdefaults in the appgroup which makes it possible to key-value observe entries. I have made an extension which defines computed properties on userdefault that are @objc marked which enables the use of combine on those computed properties. E.g. making it possible to react to changes on value changed. For pure notification it is possible to have a bool that is toggled to propagate changes (a bit hacky but works).
@AvdLee,
Thanks, I got you.
@patka817,
Yes, I'm aware about the possibility to share data via app group container, but for some types of extensions, it is not available.
I'm just wondering, is Darwin notifications mechanism is still available in this case, or not. Looks like the only way is to try it.
@OleksandStepanov Did a quick test, seems to work. Not sure about that MMWormHole. Tested on iPhone 8 with iOS 14.2, although only debug build.
@patka817,
Cool, thank you very much! We will give it a try in our project too.
@AvdLee : Thank you for this awesome work.
I just made a fork integrating Combine support.
Not sure it's the best implementation (it's my first custom publisher), but it works™.
Usage:
let center = DarwinNotificationCenter.shared
let name = DarwinNotification.Name("MarvelIsAnAwesomeDog")
let cancellable = center.publisher(for: name)
.sink { notification in
print("Notification received: ", notification)
}
// later...
cancellable.cancel()
https://gist.github.com/florentmorin/35b15837cd4fb2a2a0630dbdf41d09aa
Hi @AvdLee
Will it works safari extension too?
@pandiyaraji2i what happened when you tried it out? Didn't it work?
Hello @AvdLee,
Do you know, does Darwin notifications mechanism is still viable approach for communication between the container app and extension on iOS? I've seen some controversial information on web, particularly there are some unresolved issues on MMWormhole project, which is used the same approach. There are reports that starting from iOS 13 this mechanism had been stopped working for container app <-> extension. Any information would be much appreciated.
With regards.
My app (Proxyman for iOS) is heavily using MMWormhole to communicate between the main iOS app and its Network Extension.
Confirmed that MMWormhole still works very well on iOS 16.4.1. Tested on iPhone 13 Pro and iPad Mini 5.
@AvdLee Thank you for this great working example.
One thing I faced with is duplication in subscriptions because func unobserve()
works incorrectly.
I noticed in func observe()
we get pointer using Unmanaged.passUnretained(self).toOpaque()
but in func unobserve()
we get the pointer by using ampersand operation from self
.
To get it fixed I changed this line
https://gist.github.com/AvdLee/07de0b0fe7dbc351541ab817b9eb6c1c#file-darwinnotificationcenter-swift-L216
to let observer = Unmanaged.passUnretained(self).toOpaque()
.
The observer removal logic crashes when you try to remove an observer from its deinit, like
final class SomeClass {
deinit() {
DarwinNotificationCenter.shared.removeObserver(self)
}
}
This is because the reference to self
passed here is captured by queue.async
work block, but it's also not valid after deinit exits. Trying to access it results in a crash. The safer safe version of the removeObserver
method:
public func removeObserver(_ observer: AnyObject, for name: DarwinNotification.Name? = nil) {
cleanupObservers()
// Pointer to be compared against observation.observer pointer.
// We compare pointers because we want to avoid capturing `observer` instance in the queue.async work block.
// We need to avoid that because this method might called from the observer's `deinit`, which instance is not valid after deinit exits.
// When the queue.async work block tries to access it, there's a crash — basically, it tries to access a dangling pointer.
let observerPointer = Unmanaged.passUnretained(observer).toOpaque()
queue.async {
self.observations = self.observations.filter { (observation) -> Bool in
var pointersAreDifferent = true
if let observerInside = observation.observer {
let observationPointer = Unmanaged.passUnretained(observerInside).toOpaque()
pointersAreDifferent = observerPointer != observationPointer
}
let namesAreDifferent = name != nil && observation.name != name
let shouldRetain = pointersAreDifferent || namesAreDifferent
if !shouldRetain {
observation.unobserve()
}
return shouldRetain
}
}
}
Nicely done, just one thing: won't this post the same notification multiple times if you have multiple observers for the same notification name? Since every observation (one per observer) will get the callback for the given name which in turn will call the shared DarwinNotificationCenter which in turn will call all observers for that notification name.. 🤔