Hi everyone,
I've developed a pitch with @Philippe_Hausler to introduce Swift Concurrency to NotificationCenter
.
Please let us know your thoughts, and thanks for reading.
Concurrency-Safe Notifications
- Proposal: SF-NNNN
- Author(s): Philippe Hausler, Christopher Thielen
- Review Manager: TBD
- Status: Draft
Introduction
The NotificationCenter
API provides the ability to decouple code through a pattern of "posting" notifications and "observing" them. It is highly-integrated throughout frameworks on macOS, iOS, and other Darwin-based systems.
Posters send notifications identified by Notification.Name
, and optionally include a payload in the form of object
and userInfo
fields.
Observers receive notifications by registering code blocks or providing closures for a given identifier. They may also provide an optional OperationQueue
for the observer to execute on.
This proposal aims to improve the safety of NotificationCenter
in Swift by providing explicit support for Swift Concurrency and by adopting stronger typing for notifications.
Motivation
Idiomatic Swift code uses a number of features that help maintain program correctness and catch errors at compile-time.
For NotificationCenter
:
- Compile-time concurrency checking: Notifications today rely on an implicit contract that an observer's code block will run on the same thread as the poster, requiring the client to look up concurrency contracts in documentation, or defensively apply concurrency mechanisms which may or may not lead to issues. Notifications do allow the observer to specify an
OperationQueue
to execute on, but this concurrency model does not provide compile-time checking and may not be desirable to clients using Swift Concurrency. - Stronger types: Notifications do not use very strong types, neither in the notification identifier nor the notification's payload. Stronger typing can help the compiler validate that the expected notification is being used and avoid the possibility of spelling mistakes that come with using strings as identifiers. Stronger typing can also reduce the number of times a client needs to cast an object from one type to another when working with notification payloads.
Well-written Swift code strongly prefers being explicit about concurrency isolation and types to help the compiler ensure program safety and correctness.
Proposed solution and example
We propose a new protocol, NotificationCenter.Message
, which allows the creation of types that can be posted and observed using NotificationCenter
. NotificationCenter.Message
provides support for isolation in Swift Concurrency and is designed to interoperate with the existing Notification
type for easy adoption.
NotificationCenter.Message
is created by specifying a name of type Notification.Name
:
struct EventDidOccur: NotificationCenter.Message {
static var name: Notification.Name { eventDidOccurNotification }
}
Providing a Notification.Name
enables NotificationCenter.Message
to interoperate with posters and observers of the existing Notification
type.
By default, observers of types conforming to NotificationCenter.Message
will be observed on MainActor
, and other isolations can be expressed as well:
struct EventDidOccur: NotificationCenter.Message {
static var name: Notification.Name { eventDidOccurNotification }
static var isolation: EventActor { EventActor.shared }
}
This information is used by the compiler to ensure isolation:
NotificationCenter.default.addObserver(EventDidOccur.self) { message, isolation in
// This is bound to the isolation of EventActor as specified by EventDidOccur
}
When a NotificationCenter.Message
shares its name with an existing Notification
, its observers will be called when either NotificationCenter.Message
or Notification
is posted. To make this behavior transparent to any observers, you can optionally define a static makeMessage(:Notification)
method to transform the contents of a posted Notification
into a NotificationCenter.Message
:
struct EventDidOccur: NotificationCenter.Message {
...
static func makeMessage(_ notification: Notification) -> Self? {
// Transform notification.userInfo? into stored properties, etc.
guard let contents = notification.userInfo?["contents"] as? String else {
return nil
}
...
}
}
You can also offer the reverse, posting a NotificationCenter.Message
and transforming its contents for observers expecting the existing Notification
type, e.g. observers in Objective-C code:
struct EventDidOccur: NotificationCenter.Message {
...
static func makeNotification(_ message: Self) -> Notification {
// Transform stored properties into notification.object? and notification.userInfo?
return Notification(name: Self.name, object: message.someProperty, userInfo: ["payload": message.someOtherProperty])
}
}
Here's an example of adapting the existing NSWorkspace.willLaunchApplicationNotification Notification
to use NotificationCenter.Message
:
extension NSWorkspace {
// Bound to MainActor by default
public struct WillLaunchApplication: NotificationCenter.Message {
public static var name: Notification.Name { NSWorkspace.willLaunchApplicationNotification }
public var workspace: NSWorkspace
public var runningApplication: NSRunningApplication
init(workspace: NSWorkspace, runningApplication: NSRunningApplication) {
self.workspace = workspace
self.runningApplication = runningApplication
}
static func makeMessage(_ notification: Notification) -> Self? {
guard let workspace = notification.object as? NSWorkspace,
let runningApplication = notification.userInfo?["applicationUserInfoKey"] as? NSRunningApplication
else { return nil }
self.init(workspace: workspace, runningApplication: runningApplication)
}
static func makeNotification(_ message: Self) -> Notification {
return Notification(name: Self.name, object: message.workspace, userInfo: ["applicationUserInfoKey": message.runningApplication])
}
}
}
This notification could be observed by a client using:
NotificationCenter.default.addObserver(WillLaunchApplication.self) { message in
// Do something with message.runningApplication ...
}
And it could be posted using:
NotificationCenter.default.post(
WillLaunchApplication(workspace: someWorkspace, runningApplication: someRunningApplication)
)
Read on
You can read the full pitch including the detailed design in the pull request on the swift-foundation repository.