WWDC Quick Look 💓 By SwiftGGTeam
Meet MusicKit for Swift

Meet MusicKit for Swift

观看原视频

Highlight

Apple 在 WWDC21 推出 MusicKit for Swift,开发者可以用 Swift 并发和 SwiftUI 请求 Apple Music 内容、处理授权与订阅状态、播放目录音乐,并在 App 内展示订阅优惠。

核心内容

以前把 Apple Music 接进 App,开发者要同时处理几类问题。

内容请求是一类。你要调用 Apple Music API,解析 JSON,再把结果转成 App 里可用的模型。专辑、歌曲、艺人、流派之间还有关系。想拿到专辑曲目,就要自己组织请求和解析逻辑。

授权和 token 是另一类。Apple Music API 需要 developer token。过去的做法是到开发者后台创建 MusicKit 私钥,把私钥放到自己的服务器,再让 App 向服务器请求 token。涉及用户资料或音乐资料库时,还要处理用户同意和 user token。

播放也有选择。社交 App 可能想控制系统音乐 App 正在播放的内容。健身 App 可能需要自己的播放队列,不影响系统音乐 App。订阅状态还会影响按钮是否可点,未订阅用户需要看到合适的订阅入口。

MusicKit for Swift 把这些分散的工作收进一个 Swift 框架。它提供强类型音乐模型、结构化请求、通用数据请求、自动 token 生成、订阅状态流、播放 API 和 SwiftUI 订阅优惠修饰符。

这场演讲的示例是一个专辑 App。用户可以搜索 Apple Music 专辑,查看曲目,点击播放。后来 App 增加了扫码功能:用相机识别旧 CD 的条形码,再用 MusicKit 按 UPC 查询 Apple Music 目录,把实体唱片对应到数字专辑。

这个例子说明 MusicKit 的定位:它让音乐内容像普通 Swift 数据一样进入 App。开发者关心搜索、展示、播放和订阅流程,不需要自己从底层 API 开始搭桥。

详细内容

1. 音乐模型:专辑属性、关系和关联

01:23)MusicKit 提供新的 model layer(模型层)来访问 music items(音乐项目)。请求可以搜索 Apple Music 目录,也可以根据过滤条件抓取资源。返回结果放在 collection(集合)里,集合内建 pagination(分页)支持。

演讲用 Album 说明音乐模型的结构。Album 是 value type(值类型),包含三类数据:普通属性、relationships(关系)和 associations(关联)。普通属性包括 titleisCompilationartwork。关系包括 artistsgenrestracks。关联更偏编辑场景,例如 appearsOnrelatedAlbums,集合本身还可能带标题。

02:56)加载关系时,MusicKit 使用 with 方法。它会通过网络从 Apple Music API 获取更完整的专辑表示。

// Loading and accessing relationships

let detailedAlbum = try await album.with([.artists, .tracks, .relatedAlbums])
print("\(detailedAlbum)")

if let tracks = detailedAlbum.tracks {
    print("  Tracks:")
    tracks.prefix(2).forEach { track in
        print("    \(track)")
    }
}

关键点:

  • album.with([.artists, .tracks, .relatedAlbums]) 请求同一个专辑的更完整版本。
  • try await 表示这一步会执行异步网络请求,并且可能抛出错误。
  • .artists.tracks.relatedAlbums 是要一起加载的关系与关联。
  • detailedAlbum.tracks 是可选值,只有请求结果包含曲目时才进入 if let
  • tracks.prefix(2) 只取前两首歌,示例用它打印少量结果。
  • forEach { track in ... } 像遍历普通数组一样遍历 MusicKit 返回的集合。

03:31)访问关联的方式相同。差异在于关联集合通常有标题,示例通过 relatedAlbums.title 读取它。

// Loading and accessing associations

let detailedAlbum = try await album.with([.artists, .tracks, .relatedAlbums])
print("\(detailedAlbum)")

if let relatedAlbums = detailedAlbum.relatedAlbums {
    print("  \(relatedAlbums.title ?? ""):")
    relatedAlbums.prefix(2).forEach { relatedAlbum in
        print("    \(relatedAlbum)")
    }
}

关键点:

  • relatedAlbums 来自 album.with 请求中指定的 .relatedAlbums
  • if let relatedAlbums = detailedAlbum.relatedAlbums 保证只有存在关联数据时才访问。
  • relatedAlbums.title ?? "" 读取关联集合的标题,没有标题时打印空字符串。
  • relatedAlbums.prefix(2) 取前两个相关专辑。
  • relatedAlbum 是 MusicKit 的音乐项目,可以直接打印,也可以继续进入详情页。

2. 结构化请求:用 UPC 从条形码找到专辑

05:46)演讲中的 App 已经能识别旧 CD 的条形码。缺少的一步,是把识别到的 barcode value 变成 Apple Music 里的专辑。

MusicKit 提供 MusicCatalogResourceRequest。示例创建一个专辑请求,并把 UPC 属性和 detectedBarcode 做等值匹配。UPC 是 Universal Product Code,也就是条形码。

let albumsRequest = MusicCatalogResourceRequest<Album>(
    matching: \Album.upc,
    equalTo: detectedBarcode
)

let albumsResponse = try await albumsRequest.response()

if let firstAlbum = albumsResponse.items.first {
    await handleDetectedAlbum(firstAlbum)
}

关键点:

  • MusicCatalogResourceRequest<Album> 表示这次请求要找的是专辑资源。
  • matching: \Album.upc 指定过滤字段为专辑的 UPC。
  • equalTo: detectedBarcode 把相机识别出的条形码作为查询条件。
  • try await albumsRequest.response() 异步执行请求并取得响应。
  • albumsResponse.items.first 从返回集合里取第一个专辑。
  • await handleDetectedAlbum(firstAlbum) 调用主线程相关的处理逻辑,演讲中这个方法关闭扫码视图并推入专辑详情页。

3. 通用数据请求:访问任意 Apple Music API URL

07:47)结构化请求适合常见场景。遇到任意 Apple Music API endpoint(端点)时,MusicKit 还提供 MusicDataRequest。它接收 URLRequest,返回原始 JSON 数据。

09:02)示例加载顶层流派列表。Apple Music API 返回 JSON,代码用 JSONDecoder 解码。Genre 本身符合 Decodable,所以响应结构只需要声明一个 data 数组。

// Loading top level genres

struct MyGenresResponse: Decodable {
    let data: [Genre]
}

let countryCode = try await MusicDataRequest.currentCountryCode
let url = URL(string: "https://api.music.apple.com/v1/catalog/\(countryCode)/genres")!

let dataRequest = MusicDataRequest(urlRequest: URLRequest(url: url))
let dataResponse = try await dataRequest.response()

let decoder = JSONDecoder()
let genresResponse = try decoder.decode(MyGenresResponse.self, from: dataResponse.data)
print("\(genresResponse.data[9])")

关键点:

  • MyGenresResponse 映射 API 返回的顶层 JSON 结构。
  • let data: [Genre] 复用 MusicKit 的 Genre 类型。
  • MusicDataRequest.currentCountryCode 取得当前用户对应的国家或地区代码。
  • URL(string: ...) 拼出 Apple Music API 的流派端点。
  • MusicDataRequest(urlRequest:) 用普通 URLRequest 创建 MusicKit 数据请求。
  • try await dataRequest.response() 执行网络请求并返回响应。
  • JSONDecoder().decode(...) 把原始 dataResponse.data 解码成强类型 Swift 值。
  • genresResponse.data[9] 访问第十个流派项目。

4. 授权与 token:先请求用户同意,再让 MusicKit 管理 token

09:56)Apple 强调,用户要控制哪些 App 可以访问 Apple Music 数据。访问用户的收听历史或音乐资料库前,App 必须请求用户同意。这个授权按设备和 App 分开。

授权弹窗会显示 App 在 Info.plist 里配置的 Apple Music usage description(用途说明)。这个文案要说明 App 为什么需要访问 Apple Music。

10:49)请求授权使用 MusicAuthorization.request()

// Requesting user consent for MusicKit

@State var isAuthorizedForMusicKit = false

func requestMusicAuthorization() {
    detach {
        let authorizationStatus = await MusicAuthorization.request()
        if authorizationStatus == .authorized {
            isAuthorizedForMusicKit = true
        } else {
            // User denied permission.
        }
    }
}

关键点:

  • @State var isAuthorizedForMusicKit = false 用 SwiftUI 状态记录授权结果。
  • requestMusicAuthorization() 是触发授权流程的方法。
  • detach { ... } 创建异步执行块。
  • await MusicAuthorization.request() 请求 Apple Music 授权。
  • authorizationStatus == .authorized 表示用户已授权。
  • isAuthorizedForMusicKit = true 让依赖 MusicKit 的功能进入可用状态。
  • else 分支处理用户拒绝授权的情况。

11:21)token 处理也变简单了。过去 developer token 需要私钥、服务器和 App 到服务器的请求链路。MusicKit for Swift 可以自动生成 developer token。开发者只需要在注册 App ID 的页面进入 App Services,启用 MusicKit 复选框。

个性化端点还需要 user token。演讲说明,user token 也会由 MusicKit 自动生成。

let authorizationStatus = await MusicAuthorization.request()

// Developer token: enable MusicKit in App Services for the App ID.
// User token: MusicKit generates it for personalized endpoints.

关键点:

  • MusicAuthorization.request() 是访问用户 Apple Music 数据前的授权入口。
  • developer token 的生成交给 MusicKit,但 App ID 必须启用 MusicKit 服务。
  • user token 用于 personalized endpoint(个性化端点)。
  • 开发者不需要把 MusicKit 私钥放到自己的服务器上生成 token。

5. 订阅状态:用能力控制播放按钮

12:23)App 需要知道用户是否有 Apple Music 订阅。MusicKit 把订阅信息拆成三个能力:用户能否播放 Apple Music 目录内容,是否启用 iCloud Music Library,是否可以成为订阅者。

播放按钮只关心 canPlayCatalogContent。如果用户不能播放目录内容,按钮应该禁用。

12:54)示例用 MusicSubscription.subscriptionUpdates 持续接收订阅变化。

// Using music subscription to drive state of a play button

@State var musicSubscription: MusicSubscription?

var body: some View {
    Button(action: handlePlayButtonSelected) {
        Image(systemName: "play.fill")
    }
    .disabled(!(musicSubscription?.canPlayCatalogContent ?? false))
    .task {
        for await subscription in MusicSubscription.subscriptionUpdates {
            musicSubscription = subscription
        }
    }
}

关键点:

  • @State var musicSubscription: MusicSubscription? 保存当前订阅状态。
  • Button(action: handlePlayButtonSelected) 定义播放按钮。
  • Image(systemName: "play.fill") 使用系统播放图标。
  • .disabled(...) 根据订阅能力控制按钮是否可点。
  • musicSubscription?.canPlayCatalogContent ?? false 没有订阅信息时默认不能播放目录内容。
  • .task { ... } 在 SwiftUI 视图中启动异步任务。
  • for await subscription in MusicSubscription.subscriptionUpdates 监听订阅状态流。
  • musicSubscription = subscription 把最新状态写回 SwiftUI 状态,界面随之更新。

6. 播放器选择:系统播放器和 App 播放器

13:28)MusicKit 提供两个播放器:SystemMusicPlayerApplicationMusicPlayer

社交 App 可以用 SystemMusicPlayer 控制系统 Music App 正在播放的内容。健身 App 更适合用 ApplicationMusicPlayer,让播放状态和系统 Music App 分开。

演讲中提到的关键 API 能力如下。

// MusicKit playback concepts from the session
SystemMusicPlayer
ApplicationMusicPlayer

// Both players:
// - set the queue with one or more items
// - add an item to play next
// - add an item to play later
// - report now playing information
// - handle remote commands

// ApplicationMusicPlayer also owns a separate queue:
// - insert items in the middle
// - remove previously added items

关键点:

  • SystemMusicPlayer 远程控制系统 Music App。
  • 使用 SystemMusicPlayer 时,系统会把 Music App 报告为 now-playing app(正在播放的 App)。
  • ApplicationMusicPlayer 让当前 App 拥有独立播放队列。
  • 使用 ApplicationMusicPlayer 时,系统会把你的 App 报告为正在播放的 App。
  • 两个播放器都会报告 now playing 信息,并处理锁屏等系统媒体控制。
  • 两个播放器都能设置队列、添加下一首、添加稍后播放。
  • 只有 ApplicationMusicPlayer 提供更细的队列控制,例如插入中间位置或移除已添加项目。

7. 订阅优惠:在 App 内展示上下文订阅入口

14:50)未订阅 Apple Music 的用户可能无法使用 App 里的音乐功能。MusicKit 可以在 App 内展示订阅优惠。这个优惠可以绑定具体歌曲、专辑或播放列表。

15:34)示例把当前专辑的 ID 写入 MusicSubscriptionOffer.Options.itemID,并用 SwiftUI 的 musicSubscriptionOffer 修饰符展示 sheet。

// Showing contextual music subscription offer

@State var musicSubscription: MusicSubscription?
@State var isShowingOffer = false

var offerOptions: MusicSubscriptionOffer.Options {
    var offerOptions = MusicSubscriptionOffer.Options()
    offerOptions.itemID = album.id
    return offerOptions
}

var body: some View {
    Button("Show Subscription Offers", action: showSubscriptionOffer)
        .disabled(!(musicSubscription?.canBecomeSubscriber ?? false))
        .musicSubscriptionOffer(isPresented: $isShowingOffer, options: offerOptions)
}

func showSubscriptionOffer() {
    isShowingOffer = true
}

关键点:

  • musicSubscription 保存用户当前订阅状态。
  • isShowingOffer 控制订阅优惠 sheet 是否显示。
  • MusicSubscriptionOffer.Options() 创建订阅优惠配置。
  • offerOptions.itemID = album.id 把优惠绑定到当前专辑。
  • Button("Show Subscription Offers", ...) 提供展示订阅入口的按钮。
  • .disabled(!(musicSubscription?.canBecomeSubscriber ?? false)) 只有用户可以成为订阅者时才启用按钮。
  • .musicSubscriptionOffer(isPresented: $isShowingOffer, options: offerOptions) 把 SwiftUI sheet 接到视图上。
  • showSubscriptionOffer()isShowingOffer 设为 true,触发展示。

核心启发

1. 扫描实体唱片,打开数字专辑

  • 做什么:做一个 CD 或黑胶收藏管理功能,扫描条形码后显示 Apple Music 里的同款专辑。
  • 为什么值得做:演讲已经展示了 MusicCatalogResourceRequest<Album>Album.upc 查询的流程,实体收藏可以直接连接到数字播放。
  • 怎么开始:先用相机或 Vision 识别条形码,得到 detectedBarcode,再用 MusicCatalogResourceRequest<Album>(matching: \Album.upc, equalTo: detectedBarcode) 请求专辑,最后推入专辑详情页。

2. 给健身 App 加入独立音乐队列

  • 做什么:为每个训练计划配置不同 Apple Music 专辑或歌单,开始训练时启动独立播放队列。
  • 为什么值得做:演讲说明 ApplicationMusicPlayer 的播放状态独立于系统 Music App,适合健身场景。
  • 怎么开始:在训练开始前检查 musicSubscription?.canPlayCatalogContent,通过 MusicKit 找到曲目或歌单,再选择 ApplicationMusicPlayer 管理播放队列。

3. 用订阅状态驱动界面

  • 做什么:把播放按钮、加入资料库按钮、订阅按钮做成状态机,随 Apple Music 订阅能力自动切换。
  • 为什么值得做MusicSubscription.subscriptionUpdates 会持续产生订阅变化,界面可以实时响应用户登录、退出或订阅状态变化。
  • 怎么开始:在 SwiftUI 视图的 .taskfor await 监听 MusicSubscription.subscriptionUpdates,用 canPlayCatalogContent 控制播放按钮,用 canBecomeSubscriber 控制订阅入口。

4. 做一个 Apple Music 流派浏览器

  • 做什么:展示当前国家或地区的顶层音乐流派,再进入每个流派的专辑或歌曲列表。
  • 为什么值得做MusicDataRequest 可以访问任意 Apple Music API URL,Genre 又能直接参与 Decodable 解码。
  • 怎么开始:用 MusicDataRequest.currentCountryCode 拼出 /v1/catalog/{countryCode}/genres,通过 MusicDataRequest 请求数据,再用 JSONDecoder 解码成包含 [Genre] 的响应结构。

5. 在内容页展示上下文订阅优惠

  • 做什么:当用户打开某张专辑但不能播放时,显示「加入 Apple Music」按钮,并让订阅优惠突出这张专辑。
  • 为什么值得做MusicSubscriptionOffer.Options.itemID 可以绑定具体专辑,订阅入口和用户当前浏览内容相关。
  • 怎么开始:保存 album.id,构造 MusicSubscriptionOffer.Options,在按钮上添加 .musicSubscriptionOffer(isPresented:options:),并用 canBecomeSubscriber 控制按钮状态。

关联 Session

评论

GitHub Issues · utterances