使用 MusicKit 和 MediaPlayer 实现 Apple Music 音乐播放、曲库搜索及“正在播放(Now Playing)”元数据集成。适用于为 iOS 应用添加音乐搜索、Apple Music 订阅流程、播放队列管理、播放控制、远程命令处理或正在播放信息显示等场景。
MusicKit
搜索 Apple Music 曲库,使用 ApplicationMusicPlayer 管理播放,检查用户订阅状态,并通过 MPNowPlayingInfoCenter 和 MPRemoteCommandCenter 发布“正在播放”元数据。
目录
开发工作流
- 排查代码前,先核对 MusicKit App Service、Bundle Identifier、权限描述字符串(Purpose String)以及后台音频模式配置。
- 请求权限授权,并显式处理所有未授权(Non-authorized)状态。
- 在将歌曲加入播放队列前,先搜索或加载曲库内容,并检查
MusicSubscription.current状态。 - 应用内独立的播放需求优先选择
ApplicationMusicPlayer;仅在应用拥有相应界面/控制权时,再对接“正在播放”信息和远程命令。 - 充分测试已授权、已拒绝、未订阅、离线、队列失败、播放中断以及切歌等各种状态。修复触发故障的最小层级,恢复测试环境并重跑该状态矩阵。
环境配置
项目配置
- 在 Apple Developer 后台中,为应用的显式 App ID(Explicit Bundle ID)开启 MusicKit App Service,以便 MusicKit 能自动生成开发者令牌(Developer Token)。
- 在 Info.plist 中添加
NSAppleMusicUsageDescription键,说明应用访问用户媒体库的原因。 - 若需支持后台播放,请在
UIBackgroundModes中添加audio后台模式。
代码导入
import MusicKit // 曲库搜索、授权认证、播放控制
import MediaPlayer // MPRemoteCommandCenter, MPNowPlayingInfoCenter
权限授权
在访问用户音乐数据或播放 Apple Music 内容之前,必须先请求用户授权。request() 会在必要时弹出 Apple 官方的授权弹窗;若只需读取当前授权状态而无需弹窗提示,请使用 currentStatus。
func requestMusicAccess() async -> MusicAuthorization.Status {
let status = await MusicAuthorization.request()
switch status {
case .authorized:
// 已获得 MusicKit API 的完整访问权限
break
case .denied, .restricted:
// 引导用户前往“设置”中开启权限
break
case .notDetermined:
break
@unknown default:
break
}
return status
}
// 仅获取当前授权状态,不触发弹窗
let current = MusicAuthorization.currentStatus
曲库搜索
使用 MusicCatalogSearchRequest 搜索 Apple Music 曲库。曲库查询虽然可以获取 Apple Music 资源,但要播放付费曲库内容,仍必须先校验 MusicSubscription.current.canPlayCatalogContent。
func searchCatalog(term: String) async throws -> MusicItemCollection<Song> {
var request = MusicCatalogSearchRequest(term: term, types: [Song.self])
request.limit = 25
let response = try await request.response()
return response.songs
}
展示搜索结果
for song in songs {
print("\(song.title) - \(song.artistName)")
if let artwork = song.artwork {
let url = artwork.url(width: 300, height: 300)
// 从 URL 加载专辑封面
}
}
订阅检查
在向用户提供播放功能前,先检查其是否拥有有效的 Apple Music 订阅。
func checkSubscription() async throws -> Bool {
let subscription = try await MusicSubscription.current
return subscription.canPlayCatalogContent
}
// 监听订阅状态变化
func observeSubscription() async {
for await subscription in MusicSubscription.subscriptionUpdates {
if subscription.canPlayCatalogContent {
// 解锁完整播放界面
} else {
// 显示订阅引导
}
}
}
引导用户订阅 Apple Music
当用户未订阅时,弹出 Apple Music 订阅页(Subscription Offer Sheet)。
提示前需先校验 canBecomeSubscriber;若订阅页需要上下文元数据或加载失败处理逻辑,可传入 MusicSubscriptionOffer.Options 或 onLoadCompletion。
import MusicKit
import SwiftUI
struct MusicOfferView: View {
@State private var showOffer = false
var body: some View {
Button("订阅 Apple Music") {
Task {
let subscription = try? await MusicSubscription.current
showOffer = subscription?.canBecomeSubscriber == true
}
}
.musicSubscriptionOffer(
isPresented: $showOffer,
options: .default,
onLoadCompletion: { error in
if let error {
// 在应用 UI 或诊断日志中显示加载错误
print(error)
}
}
)
}
}
使用 ApplicationMusicPlayer 播放
ApplicationMusicPlayer 用于在应用内独立播放 Apple Music 内容,不会影响系统自带“音乐”应用(Music app)的播放状态。
let player = ApplicationMusicPlayer.shared
func playSong(_ song: Song) async throws {
player.queue = [song]
try await player.play()
}
func pause() {
player.pause()
}
func skipToNext() async throws {
try await player.skipToNextEntry()
}
监听播放状态
func observePlayback() {
// player.state 属于 @Observable 属性
let state = player.state
switch state.playbackStatus {
case .playing:
break
case .paused:
break
case .stopped, .interrupted, .seekingForward, .seekingBackward:
break
@unknown default:
break
}
}
队列管理
使用 ApplicationMusicPlayer.Queue 创建和操作播放队列。
// 使用多首曲目初始化队列
func playAlbum(_ album: Album) async throws {
player.queue = [album]
try await player.play()
}
// 将歌曲追加到现有队列末尾
func appendToQueue(_ songs: [Song]) async throws {
try await player.queue.insert(songs, position: .tail)
}
// 插队播放(下一首播放)
func playNext(_ song: Song) async throws {
try await player.queue.insert(song, position: .afterCurrentEntry)
}
正在播放信息
更新 MPNowPlayingInfoCenter,以便锁定屏幕、控制中心和 CarPlay 能正确显示当前播放曲目的元数据。在播放自定义音频源(非 MusicKit 来源)时这是必不可少的。如果是播放 Apple Music 内容,ApplicationMusicPlayer 会自动处理这一过程。
import MediaPlayer
func updateNowPlaying(title: String, artist: String, duration: TimeInterval, elapsed: TimeInterval) {
var info = [String: Any]()
info[MPMediaItemPropertyTitle] = title
info[MPMediaItemPropertyArtist] = artist
info[MPMediaItemPropertyPlaybackDuration] = duration
info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = elapsed
info[MPNowPlayingInfoPropertyPlaybackRate] = 1.0
info[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaType.audio.rawValue
MPNowPlayingInfoCenter.default().nowPlayingInfo = info
}
func clearNowPlaying() {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
}
设置专辑封面
func setArtwork(_ image: UIImage) {
let artwork = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
var info = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
info[MPMediaItemPropertyArtwork] = artwork
MPNowPlayingInfoCenter.default().nowPlayingInfo = info
}
远程命令中心
注册 MPRemoteCommandCenter 处理函数,响应锁屏控件、AirPods 触控手势以及 CarPlay 按钮操作。
func setupRemoteCommands() {
let center = MPRemoteCommandCenter.shared()
center.playCommand.addTarget { _ in
resumePlayback()
return .success
}
center.pauseCommand.addTarget { _ in
pausePlayback()
return .success
}
center.nextTrackCommand.addTarget { _ in
skipToNext()
return .success
}
center.previousTrackCommand.addTarget { _ in
skipToPrevious()
return .success
}
// 禁用不支持的命令
center.seekForwardCommand.isEnabled = false
center.seekBackwardCommand.isEnabled = false
}
支持进度条拖拽(Scrubbing)
func enableScrubbing() {
let center = MPRemoteCommandCenter.shared()
center.changePlaybackPositionCommand.addTarget { event in
guard let positionEvent = event as? MPChangePlaybackPositionCommandEvent else {
return .commandFailed
}
seek(to: positionEvent.positionTime)
return .success
}
}
常见错误
| 常见错误 | 修复方案 |
|---|---|
| 在配置 App Service 和权限说明字符串前进行授权调试 | 优先核验服务开启状态、Bundle ID 以及 NSAppleMusicUsageDescription。 |
| 未校验订阅状态就将曲库内容加入队列 | 先检查 canPlayCatalogContent;仅当 canBecomeSubscriber 时再展示订阅引导。 |
在应用独立播放场景中使用 SystemMusicPlayer |
请改用 ApplicationMusicPlayer;使用系统播放器会改动音乐应用的全局队列。 |
| 仅在初始化时发布一次“正在播放”元数据 | 每当曲目、总时长、播放速率或已播放时间变更时,均需及时更新。 |
| 注册了不支持的远程命令 | 应将不支持的命令禁用;支持的处理函数执行完毕后须返回 .success。 |
审查清单
- [ ] 已为应用的显式 Bundle ID 开启 MusicKit App Service
- [ ] 已在 Info.plist 中添加
NSAppleMusicUsageDescription - [ ] 在调用任何 MusicKit API 前均已先调用
MusicAuthorization.request() - [ ] 尝试播放曲库内容前已校验订阅状态
- [ ] 展示订阅弹出页前已校验
canBecomeSubscriber - [ ] 写入云端媒体库前已校验
hasCloudLibraryEnabled - [ ] 应用内独立播放使用的是
ApplicationMusicPlayer(而非SystemMusicPlayer) - [ ] 若支持后台播放,已开启 Background Audio 模式
- [ ] 每次切歌均已更新“正在播放”信息(针对自定义音频)
- [ ] 所支持命令的远程处理函数均返回了
.success - [ ] 不支持的远程命令已通过
isEnabled = false禁用 - [ ] “正在播放”信息中已提供封面图,以便在锁屏界面展示
- [ ] 已定期更新已播放时间(Elapsed Time),以保障进度条拖拽精准
- [ ] 当用户未订阅 Apple Music 时,能够展示订阅弹出页
参考资料
- 扩展模式(SwiftUI 集成、流派浏览、歌单管理):references/musickit-patterns.md
- MusicKit 框架
- 针对 Apple Music API 使用自动开发者令牌生成
- MusicAuthorization
- ApplicationMusicPlayer
- MusicCatalogSearchRequest
- MusicSubscription
- canPlayCatalogContent
- canBecomeSubscriber
- hasCloudLibraryEnabled
- MusicCatalogChartsRequest 构造函数
- [musicSubscriptionOffer(isPresented:options:onLoadCompletion:)](htt




