Type-safe NotificationCenter in Swift 6.2


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.