WWDC Quick Look 💓 By SwiftGGTeam
Explore more content with MusicKit

Explore more content with MusicKit

观看原视频

Highlight

MusicKit 在 2022 年大幅扩展了能力边界:新增 Curator 和 Radio Show 类型、搜索 Top Results 和 Suggestions、排行榜 API、音频质量元数据(Spatial Audio / Lossless),以及最重要的——完整的用户音乐资料库读写能力。

核心内容

目录内容增强

01:12

MusicKit 新增了两种内容类型:

Curator(策展人):如 Nike、Shazam、Beats by Dr. Dre 等品牌策展人,以及 Apple 编辑团队。每个 Curator 有 nameurlartworkkindeditorialexternal)属性,还有 playlists 关系可以获取该策展人创建的所有播放列表。

Radio Show(电台节目):如 “New Music Daily by Zane Lowe”,也有 playlists 关系。

Playlist 类型新增了反向关系:curatorradioShow,方便从播放列表追溯到创建者。

搜索体验升级

03:37

Top Results:在搜索请求中设置 includeTopResults = true,返回一个跨类型的最相关结果列表,按相关性排序。

var searchRequest = MusicCatalogSearchRequest(
    term: "Hello",
    types: [Artist.self, Album.self, Song.self]
)
searchRequest.includeTopResults = true
let searchResponse = try await searchRequest.response()
print("\(searchResponse.topResults)")

关键点

  • topResults 包含所有请求类型的结果,混合在一个集合中
  • 按相关性排序,不关心具体类型
  • 适合搜索界面的首屏展示

Suggestions:提供音乐相关的自动补全建议。

let request = MusicCatalogSearchSuggestionsRequest(term: "shaz")
let response = try await request.response()

关键点

  • 每个 suggestion 有 displayTerm(适合展示)和 searchTerm(用于实际搜索)
  • 用户选择 suggestion 后,用 searchTerm 执行正式搜索

排行榜 API

05:41

let request = MusicCatalogChartsRequest(
    kinds: [.dailyGlobalTop, .mostPlayed, .cityTop],
    types: [Song.self, Playlist.self]
)
let response = try await request.response()
print("\(response.playlistCharts.first)")

关键点

  • 支持 Top Songs、Top Albums、城市排行榜、每日全球 Top 100
  • 可按音乐类型筛选
  • 内置分页支持,不需要手动处理 MusicDataRequest

音频质量元数据

06:58

MusicKit 暴露了 audioVariants 属性,让你知道一首歌或专辑支持哪些音频格式:

let album =
let detailedAlbum = try await album.with(.audioVariants)
print("\(detailedAlbum.debugDescription)")

关键点

  • audioVariantsAudioVariant 数组
  • 包含 dolbyAtmos(空间音频)、lossless(无损音频)等
  • 新增 isAppleDigitalMaster 布尔值,标识最高品质数字母带

当前播放的音频质量也可以获取:

@ObservedObject var musicPlayerState = ApplicationMusicPlayer.shared.state

var body: some View {
    if musicPlayerState.audioVariant == .dolbyAtmos {
        Image("dolby-atmos-badge")
    }
}

用户资料库访问

12:21

今年最大的更新:应用可以读取用户的个人音乐资料库。

MusicLibraryRequest:从设备本地加载资料库内容,不走网络。

let request = MusicLibraryRequest<Playlist>()
let response = try await request.response()

关键点

  • 通过泛型参数指定请求的类型(Playlist、Album、Song 等)
  • 支持链式过滤和排序
  • includeDownloadedContentOnly = true 只返回已下载内容

过滤示例

var request = MusicLibraryRequest<Album>()
request.filter(matching: \.isCompilation, equalTo: true)
request.filter(matching: \.genres, contains: danceGenre)
request.includeDownloadedContentOnly = true
let response = try await request.response()

MusicLibrarySectionedRequest:按分组获取资料库内容。

var request = MusicLibrarySectionedRequest<Genre, Album>()
request.sortItems(by: \.artistName, ascending: true)
let response = try await request.response()

关键点

  • 第一个泛型参数是分组类型,第二个是内容类型
  • sortItems 对内容排序,sortSections 对分组排序
  • 每个分组通过 items 属性访问其下的内容

资料库写入操作

22:01

MusicKit 现在支持修改用户的音乐资料库:

// 添加歌曲到播放列表
Task {
    try await MusicLibrary.shared.add(selectedTrack, to: playlist)
    isShowingPlaylistPicker = false
}

支持的操作包括:

  • 添加内容到资料库
  • 创建播放列表
  • 编辑播放列表的元数据和曲目列表
  • 添加歌曲到现有播放列表

详细内容

完整的搜索界面实现

struct MusicSearchView: View {
    @State private var searchTerm = ""
    @State private var suggestions: [MusicCatalogSearchSuggestion] = []
    @State private var topResults: MusicItemCollection<MusicCatalogSearchable> = []

    var body: some View {
        NavigationStack {
            List {
                // 搜索建议
                if !suggestions.isEmpty {
                    Section("Suggestions") {
                        ForEach(suggestions) { suggestion in
                            Button(suggestion.displayTerm) {
                                searchTerm = suggestion.searchTerm
                                performSearch()
                            }
                        }
                    }
                }

                // Top Results
                if !topResults.isEmpty {
                    Section("Top Results") {
                        ForEach(topResults) { item in
                            switch item {
                            case let song as Song:
                                SongRow(song: song)
                            case let album as Album:
                                AlbumRow(album: album)
                            case let artist as Artist:
                                ArtistRow(artist: artist)
                            default:
                                EmptyView()
                            }
                        }
                    }
                }
            }
            .searchable(text: $searchTerm)
            .onChange(of: searchTerm) { _ in
                loadSuggestions()
            }
            .onSubmit(of: .search) {
                performSearch()
            }
        }
    }

    func loadSuggestions() async {
        let request = MusicCatalogSearchSuggestionsRequest(term: searchTerm)
        let response = try? await request.response()
        suggestions = response?.suggestions ?? []
    }

    func performSearch() async {
        var request = MusicCatalogSearchRequest(
            term: searchTerm,
            types: [Song.self, Album.self, Artist.self]
        )
        request.includeTopResults = true
        let response = try? await request.response()
        topResults = response?.topResults ?? []
    }
}

关键点

  • MusicCatalogSearchSuggestionsRequest 提供自动补全建议
  • includeTopResults = true 获取跨类型的最相关结果
  • topResultsMusicCatalogSearchable 类型的集合,需要用类型转换处理

资料库播放列表展示

struct LibraryPlaylistsView: View {
    @State private var playlists: MusicItemCollection<Playlist> = []

    var body: some View {
        List {
            Section(header: Text("Library Playlists").fontWeight(.semibold)) {
                ForEach(playlists) { playlist in
                    PlaylistCell(playlist)
                }
            }
        }
        .task {
            await loadPlaylists()
        }
    }

    @MainActor
    private func loadPlaylists() async throws {
        let request = MusicLibraryRequest<Playlist>()
        let response = try await request.response()
        playlists = response.items
    }
}

关键点

  • MusicLibraryRequest 从设备本地加载,不消耗网络流量
  • @MainActor 确保 UI 更新在主线程执行
  • 返回的 AlbumSong 等类型与目录请求返回的结构相同,能力一致

带过滤的资料库查询

func fetchDanceCompilations() async throws -> MusicItemCollection<Album> {
    var request = MusicLibraryRequest<Album>()

    // 只返回合辑
    request.filter(matching: \.isCompilation, equalTo: true)

    // 只返回 Dance 类型
    let danceGenre = Genre(id: " dance", name: "Dance")
    request.filter(matching: \.genres, contains: danceGenre)

    // 只返回已下载的
    request.includeDownloadedContentOnly = true

    let response = try await request.response()
    return response.items
}

关键点

  • 过滤条件可以链式组合
  • Xcode 自动补全只显示当前类型支持的 key path
  • includeDownloadedContentOnly 支持离线场景

加载扩展属性时指定来源

// 从目录加载专辑的曲目
let catalogTracks = try await album.with(.tracks)

// 从资料库加载同一专辑的曲目
let libraryTracks = try await album.with(.tracks, preferredSource: .library)

关键点

  • preferredSource: .library 优先从用户资料库加载
  • 对于只在目录或只在资料库存在的属性,不受 preferredSource 影响
  • 无论原始项来自目录请求还是资料库请求,都可以使用这个功能

核心启发

1. 给健身 App 加音乐推荐

  • 做什么:在锻炼开始前展示用户的个人推荐播放列表和最近播放内容
  • 为什么值得做:MusicKit 的 MusicPersonalRecommendationsRequestMusicRecentlyPlayedRequest 自动处理认证,一行代码就能获取个性化内容
  • 怎么开始:用 MusicPersonalRecommendationsRequest() 获取推荐,用 MusicRecentlyPlayedRequest<Song>() 获取最近播放的歌曲

2. 给社交 App 加音乐分享

  • 做什么:让用户搜索 Apple Music 目录中的歌曲,分享到聊天或动态
  • 为什么值得做MusicCatalogSearchRequestincludeTopResults 让搜索体验接近原生 Apple Music,用户不需要离开你的应用就能找到想分享的歌曲
  • 怎么开始:集成搜索建议 + Top Results,用户选择后获取歌曲的 url 属性进行分享

3. 给专注/番茄钟 App 加背景音乐

  • 做什么:根据用户资料库中的内容创建专注时段的播放列表
  • 为什么值得做MusicLibraryRequest 可以按类型、已下载状态过滤,配合 MusicLibrarySectionedRequest 按类型分组展示
  • 怎么开始:用 MusicLibrarySectionedRequest<Genre, Song>() 按音乐类型分组,让用户按心情选择

4. 给派对/活动 App 加协作播放列表

  • 做什么:让活动参与者把歌曲添加到共享播放列表
  • 为什么值得做:MusicKit 现在支持创建播放列表和添加歌曲,可以实现”大家一起来选歌”的功能
  • 怎么开始:用 MusicLibrary.shared.add(track, to: playlist) 把选中的歌曲添加到播放列表,用 MusicCatalogSearchRequest 让参与者搜索歌曲

关联 Session

评论

GitHub Issues · utterances