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(关联)。普通属性包括 title、isCompilation、artwork。关系包括 artists、genres、tracks。关联更偏编辑场景,例如 appearsOn 或 relatedAlbums,集合本身还可能带标题。
(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 提供两个播放器:SystemMusicPlayer 和 ApplicationMusicPlayer。
社交 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 视图的
.task中for 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
- Explore the catalog with the Apple Music API — 继续深入 Apple Music API 的目录搜索、建议和元数据请求。
- Explore ShazamKit — 学习用 ShazamKit 识别歌曲,并把音乐识别结果接入 App。
- Discover concurrency in SwiftUI — 理解 MusicKit 示例中
await、.task和 SwiftUI 状态更新的基础。 - Meet AsyncSequence — 深入
for await模式,理解MusicSubscription.subscriptionUpdates这类异步流。 - Meet async/await in Swift — 掌握 MusicKit 网络请求中使用的 Swift 并发语法。
评论
GitHub Issues · utterances