了解 NowPlaying 框架
Meet the Now Playing framework
2026年6月10日
一句话判断
✅ Apple 终于用 @Observable 和 Swift 协议重写了锁屏播放控件,彻底埋葬了反人类的 MPNowPlayingInfoCenter 字典 API。
这场 Session 讲了什么
做过音视频的都知道,配锁屏控件和耳机线控简直是折磨。MPNowPlayingInfoCenter 那个塞满魔法字符串的字典,加上 MPRemoteCommandCenter 一堆散落的闭包,代码写得像意大利面,状态同步全靠手动触发。现在 Apple 推出了全新的 NowPlaying 框架。你只需要让播放模型遵守 MediaSessionRepresentable 协议,配合 Swift 的 @Observable,系统就能自动把锁屏、灵动岛(Dynamic Island)、CarPlay 的 UI 同步好。
更狠的是对第三方硬件的收编。如果你做的是智能音箱或者外部播放设备,以前得自己搞一套复杂的 UI 和通信。现在通过 RemoteMediaSessionRepresentable 和 App Extension,加上 APNs(Apple Push Notification service)推送,外部设备的播放状态能直接原生显示在 iPhone 的控制中心里。顺便还推出了 Media Sharing Extensions,让你不用在 App 里塞各种投屏 SDK,直接调用系统的设备选择器(Device Picker)。这对做泛音频、IoT 硬件配套的开发者来说是史诗级减负。
值得深挖的点
从字典魔法到 @Observable 的范式转移
旧方案 MPNowPlayingInfoCenter 是基于 NSDictionary 的,键名容易拼错,类型不安全,且状态更新是命令式的——每次改个播放进度,都要重新 set 整个字典或特定 key,系统根本不知道你到底改了啥,只能无脑刷新。
新方案 MediaSessionRepresentable 是纯声明式的。结合 @Observable,你只需修改模型属性(比如 isPlaying 或 currentSound),NowPlaying 框架会自动 diff 并更新系统 UI。Apple 做这个设计的 trade-off 很明显:强制你的播放器架构向 Swift 现代并发和 Observation 靠拢。老 Objective-C 项目或者基于 Combine 的老架构迁移成本较高,但一旦切过去,那些因为状态不同步导致的“锁屏进度条卡死”、“暂停按钮不更新”的玄学 Bug 会直接消失。
智能硬件的“系统级收编” (Remote Media Session)
以前 Sonos 或 Bose 的 App 在后台时,你没法在 iOS 原生控制中心直接控制它们,或者体验很割裂,必须切回 App 才能切歌。现在 Apple 提供了 RemoteMediaSessionExtension。通过 APNs 把硬件状态推给 Extension,Extension 更新 RemotePlayerModel。这把第三方硬件的播放体验拉到了和 Apple Music 完全一致的水平。
这里的坑在于强依赖 APNs 和后端服务器的实时性。Extension 的生命周期由系统严格控制,你不能在 Extension 里维持 WebSocket 长连接。如果网络拉胯或者 APNs 推送延迟,锁屏控件的反馈就会有明显滞后感。这要求你的后端架构必须做到极低延迟的状态分发。
代码片段
1. 本地播放模型接入 NowPlaying
场景:让你的 PlayerModel 支持锁屏、控制中心和灵动岛。
import NowPlaying
extension PlayerModel: MediaSessionRepresentable {
// 会话 ID 必须保持稳定,切歌时不要变
var id: String { "ambient-sound-session" }
var content: (any MediaContentRepresentable)? {
return GenericContent(
id: sound.id,
title: sound.name,
subtitle: sound.description,
type: .audio,
duration: .live, // 直播或无限流媒体使用 .live
artwork: Artwork(id: sound.id) { size in
// 异步加载封面图,系统按需调用
let data = try await self.artworkData(size: size)
return try ArtworkRepresentation(data: data)
}
)
}
var playbackSnapshot: MediaPlaybackSnapshot {
// 只要模型属性变了,这里会自动被系统重新读取
MediaPlaybackSnapshot(isPlaying: player.isPlaying)
}
}
坑:Artwork 的闭包是异步的,千万别在里面做耗时的同步阻塞操作(比如同步读大文件),否则锁屏加载图片时会卡死系统 UI 进程。
2. 外部设备的远程会话 Extension
场景:智能音箱通过 App Extension 将状态同步到 iPhone 控制中心。
import ExtensionFoundation
import NowPlaying
@main
final class SampleAppExtension: @MainActor RemoteMediaSessionExtension {
var configuration: some AppExtensionConfiguration {
RemoteMediaSessionExtensionConfiguration(extension: self)
}
var extensionPoint: AppExtensionPoint {
// 声明这是一个 remote-media 扩展点
AppExtensionPoint.Identifier(host: "com.apple.nowplaying", name: "remote-media")
}
func session(_ state: RemotePlayerState) async throws -> RemotePlayerModel {
// 系统收到 APNs 触发时,返回你的远程播放模型
RemotePlayerModel(state: state)
}
}
坑:Extension 是按需唤醒的,不要在里面持有长连接或缓存大量数据。老老实实用 APNs 触发,并通过无状态的 HTTP 请求去服务器拉取最新状态。
最佳实践
老项目别急着全切。可以先封装一个 Adapter,把现有的 MPNowPlayingInfoCenter 调用桥接到新的 MediaSession 上,逐步把播放器核心逻辑重构为 @Observable。强行在老架构上套新壳,会导致状态更新时序错乱。
新项目直接上 NowPlaying 框架。把 MediaSession 的初始化放在播放器引擎初始化的同一个地方,确保生命周期一致。一旦引擎销毁,Session 也要跟着释放,不然系统会一直显示你的 App 在后台播放。
实战中最容易踩的坑是 MediaSession 的 id。这个 ID 代表的是“播放会话”而不是“当前歌曲”。如果每次切歌都生成新的 session id,系统会认为这是两个不同的播放会话,导致灵动岛的过渡动画断裂,锁屏控件也会闪烁重置。
还有什么值得关注
- Media Sharing Extensions:让投屏和设备路由不再需要集成臃肿的第三方 SDK,系统级 Device Picker 直接搞定,App 包体积能小不少。
- 强类型内容模型:
GenericContent、MusicContent等模型终于让编译器能帮你检查媒体元数据了,告别MPMediaItemPropertyTitle这种容易拼错的字符串。 - 自动进度条 UI:系统会根据
duration: .live或.continuous自动调整锁屏进度条的样式(比如变成直播红点),不用再手动算无穷大来隐藏进度条了。