WWDC Quick Look 💓 By SwiftGGTeam
Integrate MusicKit into your app

Integrate MusicKit into your app

观看原视频

Highlight

MusicKit 为 SwiftUI 提供了 .musicPicker().musicSubscriptionOffer() 两个原生修饰器,让非音乐类 App 也能在几行代码内完成授权、选歌、播放控制和订阅引导的完整闭环。

核心内容

以前接 Apple Music 有多麻烦

你想在健身 App 里加一段背景音乐,流程是这样的:先写授权逻辑请求用户许可,再判断用户是否订阅了 Apple Music,没订阅的话得自己设计一套引导订阅的 UI,订阅了还要手搓选歌界面、处理播放队列、监听播放状态。一套下来,为了加个 BGM 功能,可能要写上千行代码。

Apple 做了什么改变

WWDC26 的 MusicKit 把上述流程打包成了两个 SwiftUI 修饰器和一个原生选歌器。授权用 MusicAuthorization.request(),订阅引导用 .musicSubscriptionOffer(),选歌用 .musicPicker(),播放控制直接操作 ApplicationMusicPlayer.shared。整个接入流程从”重写半个 App”变成了”加几行修饰器”。

新能力如何解决痛点

以 Session 中演示的健身 App 为例:用户开始骑行训练时,点击”选歌”按钮弹出 .musicPicker(),从 Apple Music 曲库或个人资料库中挑选歌曲;如果用户没订阅,界面上自动出现订阅按钮,点击后弹出 .musicSubscriptionOffer(),用户无需离开 App 就能完成订阅;选好的歌曲通过 ApplicationMusicPlayer 播放,App 内直接显示专辑封面、歌曲信息和播放控制按钮。整个体验是原生的、无缝的。

详细内容

授权与项目配置

01:03)接入 MusicKit 的第一步是在 Xcode 中启用 MusicKit 服务。在 App ID 注册页面的 App Services 标签页勾选 MusicKit,Xcode 会自动生成开发者令牌(Developer Token),无需手动管理。

03:12)请求用户授权使用 MusicAuthorization.request(),这是一个异步方法,返回授权状态。为了让权限弹窗显示更清晰的说明,需要在 Xcode 的 Signing & Capabilities 中添加 Media Library 能力,并在描述文本框中填写 App 使用音乐库的目的。

订阅引导

04:46)对于未订阅 Apple Music 的用户,可以用 .musicSubscriptionOffer() 修饰器在 App 内直接展示订阅引导界面,用户无需跳转到 Music App 或 App Store。

@State var showSubscriptionOffer = false

let options = MusicSubscriptionOffer.Options(
    messageIdentifier: .playMusic
)

@ViewBuilder
var musicSubscriptionButton: some View {
    Button("Subscribe to Apple Music", systemImage: "music.note") {
        showSubscriptionOffer = true
    }
    .musicSubscriptionOffer(isPresented: $showSubscriptionOffer, options: options)
}

关键点:

  • MusicSubscriptionOffer.OptionsmessageIdentifier 决定弹窗展示的 UI 文案和样式,.playMusic 适合播放场景
  • 开发者可以通过 Apple Services Performance Partner Program 获得订阅佣金
  • 弹窗通过 isPresented 绑定控制显示与隐藏

05:59)在主视图中监听订阅状态,只在用户有资格订阅时显示按钮:

@State var subscription: MusicSubscription?

var body: some View {
    VStack {
        if let subscription, subscription.canBecomeSubscriber {
            musicSubscriptionButton
        }
    }
    .task(id: isAuthorized) {
        self.subscription = try? await MusicSubscription.current
        for await subscription in MusicSubscription.subscriptionUpdates {
            self.subscription = subscription
        }
    }
}

关键点:

  • MusicSubscription.current 获取当前订阅状态
  • MusicSubscription.subscriptionUpdates 是 AsyncSequence,订阅状态变化时自动推送新值
  • canBecomeSubscriber 判断用户是否有资格订阅(未订阅且所在地区支持)

音乐选择器

08:48.musicPicker() 是一个 SwiftUI 视图修饰器,呼出原生界面让用户浏览 Apple Music 曲库和个人资料库。支持单选和多选,也支持选择专辑或播放列表。

@State var showMusicPicker = false
@State var selectedSong: Song? = nil

@ViewBuilder
var musicPickerButton: some View {
    Button("Pick some Music", systemImage: "music.note.list") {
        showMusicPicker = true
    }
    .musicPicker(isPresented: $showMusicPicker, selection: $selectedSong)
}

关键点:

  • selection 绑定到 Song? 是单选,绑定到 [Song] 是多选
  • 不需要 Apple Music 订阅也能使用,但未订阅用户只能看到个人资料库中的内容
  • 支持搜索、浏览专辑详情页、选择整个播放列表

两种播放器的选择

10:54)MusicKit 提供两种播放器:

特性SystemMusicPlayerApplicationMusicPlayer
控制对象系统 Music AppApp 自身
后台播放自动支持需开启 Audio Background Mode
队列访问只读当前项完全读写
适用场景独立音乐客户端健身、冥想等需要 BGM 的 App

ApplicationMusicPlayer 的队列支持延迟加载(Lazy loading)。如果用专辑或播放列表初始化队列,播放器会按需加载其中的歌曲,而不是一次性全部加载,这能显著减少启动延迟。

13:02prepareToPlay() 方法可以在用户点击播放前预先缓冲音频资产,消除点击后的等待时间:

let player = ApplicationMusicPlayer.shared

// 提前缓冲
Task {
    try await player.prepareToPlay()
}

播放控制与状态观察

14:49ApplicationMusicPlayerqueuestate 都是可观察对象,可以直接在 SwiftUI 视图中使用。

显示当前歌曲的专辑封面:

@State var queue = ApplicationMusicPlayer.shared.queue

var body: some View {
    VStack {
        if let artwork = queue.currentEntry?.artwork {
            ArtworkImage(artwork, width: 200, height: 200)
        } else {
            RoundedRectangle(cornerRadius: 16)
                .fill(.quaternary)
                .frame(width: 200, height: 200)
        }
    }
}

关键点:

  • ArtworkImage 是 MusicKit 提供的 SwiftUI 视图,自动处理专辑封面加载和显示
  • queue.currentEntry?.artwork 获取当前播放项的封面资源
  • .quaternary 填充色做占位图,视觉过渡自然

15:06)显示歌曲标题和副标题:

if let currentSong = queue.currentEntry {
    Text(currentSong.title)
        .font(.title3.bold())

    if let subtitle = currentSong.subtitle {
        Text(subtitle)
            .font(.subheadline)
            .foregroundStyle(.secondary)
    }
}

15:14)播放/暂停控制:

let player = ApplicationMusicPlayer.shared
@State var state = ApplicationMusicPlayer.shared.state

var isPlaying: Bool {
    state.playbackStatus == .playing
}

var playPause: some View {
    Button(
        isPlaying ? "Pause" : "Play",
        systemImage: isPlaying ? "pause.fill" : "play.fill"
    ) {
        if isPlaying {
            player.pause()
        } else {
            Task {
                try await player.play()
            }
        }
    }
}

关键点:

  • state.playbackStatus 返回 .playing.paused 等枚举值
  • player.play()player.pause() 都是异步方法,需要在 Task 中调用
  • 播放状态变化会自动触发 SwiftUI 视图更新

15:38)上一首/下一首控制:

let player = ApplicationMusicPlayer.shared

var controls: some View {
    HStack {
        Button("Back", systemImage: "backward.fill") {
            Task {
                try await player.skipToPreviousEntry()
            }
        }
        // ...
        Button("Next", systemImage: "forward.fill") {
            Task {
                try await player.skipToNextEntry()
            }
        }
    }
}

曲库请求与跨区等效

16:26MusicCatalogResourceRequest 用于从 Apple Music 曲库中请求特定资源。可以指定关联数据(如歌曲的 Artists 关系)和返回数量限制。

18:58)跨区等效是 MusicKit 的一个重要特性。同一首歌曲在不同区服的 Apple Music 可能有不同的 ID,某些区服可能有”清洁版”替代” explicit 版”。使用 .findEquivalents 选项可以自动处理这些映射:

func fetchSongs(songIDs: [MusicItemID]) async throws -> (featured: Song?, other: [Song]) {
    var request = MusicCatalogResourceRequest<Song>(
        matching: \.id, memberOf: songIDs
    )
    request.options = [.findEquivalents]

    let response = try await request.response()

    let featuredSongID = songIDs[0]
    let featuredSong = response.item(for: featuredSongID)

    let others: [Song] = songIDs[1...].compactMap { songID in
        return response.item(for: songID)
    }

    return (featuredSong, others)
}

关键点:

  • MusicCatalogResourceRequest<Song> 是泛型请求,<Song> 指定返回类型
  • matching: \.id, memberOf: songIDs 表示按 ID 匹配请求列表中的歌曲
  • .findEquivalents 启用跨区等效查找,自动替换不可用的资源
  • response.item(for:) 按原始 ID 获取结果,即使实际返回的是等效版本
  • MusicItemCollection 支持分页,通过 hasNextBatchnextBatch() 获取后续结果

核心启发

1. 为健身 App 添加训练 BGM

在训练开始界面加一个”选歌”按钮,用 .musicPicker() 让用户挑选播放列表,通过 ApplicationMusicPlayer 在训练期间循环播放。利用 prepareToPlay() 在训练倒计时阶段提前缓冲,用户点击”开始”时音乐即刻响起。

入口 API:ApplicationMusicPlayer.shared + .musicPicker()

2. 构建专注计时器的白噪音/背景音乐功能

番茄钟或冥想 App 可以用 MusicCatalogResourceRequest 请求 Apple Music 上的专注歌单或白噪音专辑,展示给用户作为预设选项。用户点击即可开始播放,无需离开 App 去 Music 中搜索。

入口 API:MusicCatalogResourceRequest<Playlist> + ApplicationMusicPlayer

3. 社交 App 的”分享此刻在听”功能

queue.currentEntry 获取用户当前播放的歌曲信息,结合 ArtworkImage 生成分享卡片。通过 MusicCatalogResourceRequest.findEquivalents 选项确保分享的歌曲链接在不同区服的用户都能正常打开。

入口 API:queue.currentEntry + MusicCatalogResourceRequest + .findEquivalents

4. 游戏 App 的动态背景音乐切换

根据游戏场景(战斗、探索、剧情)预加载不同的 MusicItemCollection,通过 ApplicationMusicPlayer 的队列操作实现无缝切换。开启 Audio Background Mode 后,即使玩家切换到其他 App,音乐也能继续播放。

入口 API:ApplicationMusicPlayer.shared.queue + prepareToPlay()

5. 儿童 App 的内容安全过滤

利用 MusicCatalogResourceRequest.findEquivalents 选项,当请求的 explicit 内容在用户账号设置下不可用时,自动返回清洁版替代。无需在 App 中维护内容分级逻辑。

入口 API:MusicCatalogResourceRequest + .findEquivalents

关联 Session

评论

GitHub Issues · utterances