传统通知的痛点
老式 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 更新通知变成类型安全
- 定义消息结构体
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
即可
- 发送通知:一行代码
swift
let doc = Document()
doc.title = "New Title"
NotificationCenter.default.post(
Document.Update(title: doc.title, content: doc.content),
subject: doc
)
→ 无字符串 key、无 userInfo、无强转。
- 观察通知:返回类型就是消息结构体
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 协议 = 把通知变成结构体:字段类型自动对,编译器替你排雷。"