Greetings, traveler!
In modern Swift, broadcast messaging via NotificationCenter
has always been flexible but often error-prone. It relies on loosely-typed userInfo
dictionaries and string-based keys, creating risks of typos, type mismatches, and silent failures. Swift 6.2 introduces a new, structured solution: typed messages using the NotificationCenter.Message
protocol family. This approach provides compile-time safety, better clarity, and native support for concurrency.
The drawbacks of traditional notifications
Consider the following conventional setup:
final class Document {
var title: String
var content: String
init(title: String, content: String) {
self.title = title
self.content = content
}
}
extension Document {
static let didUpdateNotification = Notification.Name("DocumentDidUpdate")
static let titleKey = "title"
static let contentKey = "content"
}
final class Logger {
init(document: Document) {
NotificationCenter.default.addObserver(
forName: Document.didUpdateNotification,
object: document,
queue: .main
) { note in
guard
let info = note.userInfo,
let title = info[Document.titleKey] as? String,
let content = info[Document.contentKey] as? String
else { return }
print("Updated \(title): \(content.prefix(30))…")
}
}
}
This setup works, but it comes with pitfalls:
- Manually defined keys might have typos.
- Information is stored in an untyped dictionary.
- Developers must manually cast values, risking runtime issues.
Typed Message Structs: A Swift 6.2 solution
Swift 6.2 enhances NotificationCenter
with a Message
protocol that enables defining notifications as typed structs:
@available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *)
extension NotificationCenter {
public protocol Message {
associatedtype Subject
static var name: Notification.Name { get }
static func makeMessage(_ notification: Notification) -> Self?
static func makeNotification(_ message: Self, object: Self.Subject?) -> Notification
}
}
@available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *)
extension NotificationCenter {
public protocol MainActorMessage : NotificationCenter.Message { }
public protocol AsyncMessage : Sendable, NotificationCenter.Message { }
}
Let’s redefine our previous example using this model:
extension Document {
struct Update: NotificationCenter.MainActorMessage {
typealias Subject = Document
static var name: Notification.Name { Document.didUpdateNotification }
let title: String
let content: String
}
}
Observing and posting become safer and more expressive:
struct ContentView: View {
@State private var token: NotificationCenter.ObservationToken?
@State private var text = ""
@State private var doc = Document(
title: "Test Title",
content: "Test Content"
)
var body: some View {
VStack {
Text(text)
Button("Tap me") {
doc.title = "Another Title"
NotificationCenter.default.post(
Document.Update(
title: doc.title,
content: doc.content
),
subject: doc
)
}
}
.onAppear {
token = NotificationCenter.default.addObserver(
of: doc,
for: Document.Update.self
) { message in
text = "Document now titled \(message.title) with content length \(message.content.count)"
}
}
}
}
Conclusion
Adopting this pattern makes NotificationCenter-based communication more robust, expressive, and aligned with Swift’s direction toward safety and clarity.
If you enjoyed this article, please feel free to follow me on my social media: