Swift 6.2 类型安全 NotificationCenter:告别字符串撞车

传统通知的痛点

老式 NotificationCenter 三板斧:

swift 复制代码
import Foundation
extension Notification.Name {
    // 这里的字符创容易拼接错误
    static let didUpdate = Notification.Name("DocumentDidUpdate")
}

class Doc {}
let doc = Doc()
let note = Notification(name: .didUpdate,
                        object: doc,
                        userInfo: ["title": "Hi", "content": "Text"])
NotificationCenter.default.addObserver(forName: .didUpdate, object: nil, queue: nil) { notity in
    // 监听消息 这里需要类型转换
    let userInfo = notity.userInfo as? [String: Any] ?? [:]
    // 这里只是一层title,如果还有第二层的属性,还需要as?转换
    let name = (userInfo["title"] as? [String: String])?["name"]
    print(name ?? "no name")
}

问题清单:

  • 字符串 key 易拼错 → 运行时 nil
  • 手动 as? 强转 → 类型错也 nil
  • userInfo 可选链地狱 → 代码臃肿
  • 无并发安全 → Sendable 检查红成海

Swift 6.2 给出官方解:NotificationCenter.Message 协议族 ------ 把通知变成强类型结构体。

Swift 6.2 新武器:Message 协议

swift 复制代码
@available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *)
extension NotificationCenter {
    ///
    /// For example, if there exists a ``Notification`` posted on `MainActor` identified by the ``Notification/Name`` `"eventDidFinish"` with a ``Notification/userInfo``
    /// dictionary containing the key `"duration"` as an ``NSNumber``, an app could post and observe the notification with the following ``MainActorMessage``:
    ///
    /// ```swift
    /// struct EventDidFinish: NotificationCenter.MainActorMessage {
    ///     typealias Subject = Event
    ///     static var name: Notification.Name { Notification.Name("eventDidFinish") }
    ///
    ///     var duration: Int
    ///
    ///     static func makeNotification(_ message: Self) -> Notification {
    ///         return Notification(name: Self.name, userInfo: ["duration": NSNumber(message.duration)])
    ///     }
    ///
    ///     static func makeMessage(_ notification: Notification) -> Self? {
    ///         guard let userInfo = notification.userInfo,
    ///               let duration = userInfo["duration"] as? Int
    ///         else {
    ///             return nil
    ///         }
    ///
    ///         return Self(duration: duration)
    ///     }
    /// }
    /// ```
    ///
    /// With this definition, an observer for this `MainActorMessage` type receives information even if the poster used the ``Notification`` equivalent, and vice versa.
    public protocol MainActorMessage : SendableMetatype {

        /// A type which you can optionally post and observe along with this `MainActorMessage`.
        associatedtype Subject

        /// A optional name corresponding to this type, used to interoperate with notification posters and observers.
        static var name: Notification.Name { get }

        /// Converts a posted notification into this main actor message type for any observers.
        ///
        /// To implement this method in your own `MainActorMessage` conformance, retrieve values from the ``Notification``'s ``Notification/userInfo`` and set them as properties on the message.
        /// - Parameter notification: The posted ``Notification``.
        /// - Returns: The converted `MainActorMessage` or `nil` if conversion is not possible.
        @MainActor static func makeMessage(_ notification: Notification) -> Self?

        /// Converts a posted main actor message into a notification for any observers.
        ///
        /// To implement this method in your own `MainActorMessage` conformance, use the properties defined by the message to populate the ``Notification``'s ``Notification/userInfo``.
        /// - Parameters:
        ///   - message: The posted `MainActorMessage`.
        /// - Returns: The converted ``Notification``.
        @MainActor static func makeNotification(_ message: Self) -> Notification
    }
}

@available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *)
extension NotificationCenter {
    /// For example, if there exists a ``Notification`` posted on an arbitrary isolation identified by the ``Notification/Name`` `"eventDidFinish"` with a ``Notification/userInfo``
    /// dictionary containing the key `"duration"` as an ``NSNumber``, an app could post and observe the notification with the following ``AsyncMessage``:
    ///
    /// ```swift
    /// struct EventDidFinish: NotificationCenter.AsyncMessage {
    ///     typealias Subject = Event
    ///     static var name: Notification.Name { Notification.Name("eventDidFinish") }
    ///
    ///     var duration: Int
    ///
    ///     static func makeNotification(_ message: Self) -> Notification {
    ///         return Notification(name: Self.name, userInfo: ["duration": NSNumber(message.duration)])
    ///     }
    ///
    ///     static func makeMessage(_ notification: Notification) -> Self? {
    ///         guard let userInfo = notification.userInfo,
    ///               let duration = userInfo["duration"] as? Int
    ///         else {
    ///             return nil
    ///         }
    ///
    ///         return Self(duration: duration)
    ///     }
    /// }
    /// ```
    ///
    /// With this definition, an observer for this `AsyncMessage` type receives information even if the poster used the ``Notification`` equivalent, and vice versa.
    public protocol AsyncMessage : Sendable {

        /// A type which you can optionally post and observe along with this `AsyncMessage`.
        associatedtype Subject

        /// A optional name corresponding to this type, used to interoperate with notification posters and observers.
        static var name: Notification.Name { get }

        /// Converts a posted notification into this asynchronous message type for any observers.
        ///
        /// To implement this method in your own `AsyncMessage` conformance, retrieve values from the ``Notification``'s ``Notification/userInfo`` and set them as properties on the message.
        /// - Parameter notification: The posted ``Notification``.
        /// - Returns: The converted `AsyncMessage`, or `nil` if conversion is not possible.
        static func makeMessage(_ notification: Notification) -> Self?

        /// Converts a posted asynchronous message into a notification for any observers.
        ///
        /// To implement this method in your own `AsyncMessage` conformance, use the properties defined by the message to populate the ``Notification``'s ``Notification/userInfo``.
        /// - Parameters:
        ///   - message: The posted `AsyncMessage`.
        /// - Returns: The converted ``Notification``.
        static func makeNotification(_ message: Self) -> Notification
    }
}

实现一个 Message 结构体 = 定义通知 Schema,编译器自动生成转换逻辑。

实战:把 Document 更新通知变成类型安全

  1. 定义消息结构体
swift 复制代码
class Document {
    var title: String = "title"
    var content: String = "content"
}

extension Document {
    struct Update: NotificationCenter.MainActorMessage {
        typealias Subject = Document          // 关联对象类型
        // 这里的name依然使用了字符串,这个不能避免
        static var name: Notification.Name { .init("DocumentDidUpdate") }
        
        let title: String                     // 强类型字段
        let content: String
    }
}
  • MainActorMessage → 回调保证在主线程
  • 若无需主线程 → 改遵 AsyncMessage 即可
  1. 发送通知:一行代码
swift 复制代码
let doc = Document()
doc.title = "New Title"
NotificationCenter.default.post(
    Document.Update(title: doc.title, content: doc.content),
    subject: doc
)

→ 无字符串 key、无 userInfo、无强转。

  1. 观察通知:返回类型就是消息结构体
swift 复制代码
struct ContentView: View {
    @State private var token: NotificationCenter.ObservationToken?
    @State private var text = ""
    @State private var doc = Document(title: "Hi", content: "Text")

    var body: some View {
        VStack {
            Text(text)
            Button("Update") {
                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 = "标题: \(message.title)  长度: \(message.content.count)"
            }
        }
    }
}

亮点:

  • 回调参数 message 已是 Document.Update → 字段类型自动对
  • 拼写错误 → 编译期报错
  • 观察令牌 ObservationToken 生命周期随 View → 自动移除监听

并发安全:Sendable 一步到位

swift 复制代码
extension Document.Update: AsyncMessage { }   // 空实现即可
  • 因为结构体里所有字段都遵守 Sendable → 自动满足
  • 回调在后台线程执行也无数据竞争

若字段含非 Sendable 类型,编译器会立刻报错,防止"带病上线"。

向后兼容 & 迁移策略

场景 做法
老代码大量字符串通知 先加 @preconcurrency import Foundation 静默警告,再逐步封装 Message
需要支持旧系统 保留原 Notification.Name + userInfo,新旧 API 并存
模块边界 把 Message 定义在 public extension 里,供外部模块使用

迁移口诀: "先封装 Message,再替换 post/observer,最后删除字符串 key。"

常见编译错误对照

错误 原因 修复
Type 'Update' does not conform to protocol 'Sendable' 字段含非 Sendable 类型 给字段加 Sendable 或改用 class + @unchecked Sendable
Call to main actor-isolated instance method in a synchronous context MainActorMessage 但回调里调 UI 确保回调已加 @MainActor 或包进 MainActor.run
Cannot find 'addObserver' in scope Deployment Target < macOS 26/iOS 26 新 API 仅 Swift 6.2 + 系统版本可用,老系统用旧 API

什么时候用 / 不用

✅ 强烈推荐

  • 新工程全部上新 Message
  • 老工程逐步迁移高价值通知(配置、用户状态)
  • 需要跨模块广播且字段较多

❌ 可以暂缓

  • 仅发送一次且字段极少的通知(如 "appDidBecomeActive"
  • 需要支持远古系统(iOS < 18)

一句话总结

"Message 协议 = 把通知变成结构体:字段类型自动对,编译器替你排雷。"

相关推荐
HarderCoder3 小时前
Swift 控制流深度解析(一):循环、条件与分支
swift
HarderCoder3 小时前
Swift 控制流深度解析(二):模式匹配、并发与真实项目套路
swift
QWQ___qwq1 天前
SwiftUI 的状态管理包装器(Property Wrapper)
ios·swiftui·swift
大熊猫侯佩2 天前
AI 开发回魂夜:捉鬼大师阿星的 Foundation Models 流式秘籍
llm·ai编程·swift
JZXStudio3 天前
4.布局系统
框架·swift·app开发
HarderCoder3 天前
Swift 函数完全指南(四):从 `@escaping` 到 `async/await`——打通“回调→异步→并发”任督二脉
swift
HarderCoder3 天前
Swift 函数完全指南(三):`@autoclosure`、`rethrows`、`@escaping` 与内存管理
swift
HarderCoder3 天前
Swift 函数完全指南(二):泛型函数与可变参数、函数重载、递归、以及函数式编程思想
swift