musickit

musickit

热门

使用 MusicKit 和 MediaPlayer 实现 Apple Music 音乐播放、曲库搜索及“正在播放(Now Playing)”元数据集成。适用于为 iOS 应用添加音乐搜索、Apple Music 订阅流程、播放队列管理、播放控制、远程命令处理或正在播放信息显示等场景。

967Star
49Fork
更新于 2026/7/31
SKILL.md
只读
名称
musickit
描述

使用 MusicKit 和 MediaPlayer 实现 Apple Music 音乐播放、曲库搜索及“正在播放(Now Playing)”元数据集成。适用于为 iOS 应用添加音乐搜索、Apple Music 订阅流程、播放队列管理、播放控制、远程命令处理或正在播放信息显示等场景。

MusicKit

搜索 Apple Music 曲库,使用 ApplicationMusicPlayer 管理播放,检查用户订阅状态,并通过 MPNowPlayingInfoCenterMPRemoteCommandCenter 发布“正在播放”元数据。

目录

开发工作流

  1. 排查代码前,先核对 MusicKit App Service、Bundle Identifier、权限描述字符串(Purpose String)以及后台音频模式配置。
  2. 请求权限授权,并显式处理所有未授权(Non-authorized)状态。
  3. 在将歌曲加入播放队列前,先搜索或加载曲库内容,并检查 MusicSubscription.current 状态。
  4. 应用内独立的播放需求优先选择 ApplicationMusicPlayer;仅在应用拥有相应界面/控制权时,再对接“正在播放”信息和远程命令。
  5. 充分测试已授权、已拒绝、未订阅、离线、队列失败、播放中断以及切歌等各种状态。修复触发故障的最小层级,恢复测试环境并重跑该状态矩阵。

环境配置

项目配置

  1. 在 Apple Developer 后台中,为应用的显式 App ID(Explicit Bundle ID)开启 MusicKit App Service,以便 MusicKit 能自动生成开发者令牌(Developer Token)。
  2. 在 Info.plist 中添加 NSAppleMusicUsageDescription 键,说明应用访问用户媒体库的原因。
  3. 若需支持后台播放,请在 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.OptionsonLoadCompletion

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 时,能够展示订阅弹出页

参考资料