MusicKitとは
MusicKitとはクライアントアプリとApple Musicを統合することができるライブラリで、
Apple Music内の音楽情報にアクセスすることができ、音楽の再生や停止が可能。
Use MusicKit to integrate your app with Apple Music API, a web service you use to access information about music items in the Apple Music catalog. Using MusicKit, you can more easily build apps that tie into Apple Music.
今回作るアプリ
- ライブラリの曲情報の一覧を取得して表示
- オススメのアルバム情報の一覧を取得して表示
- オススメのプレイリスト情報の一覧を取得して表示
- オススメのステーション情報の一覧を取得して表示
- ライブラリの曲情報の一覧のアートワークを押下するとその曲を再生できる
- 再生中の曲を一時停止/再開できる

事前準備
1. App ServiceのMusicKitを有効化し、開発者トークンを自動生成するようにする
MusicKitを有効化することでApple Music APIにリクエストする際に必要な開発者トークンを自動生成してくれるようになる。
①
Identifiersの右側にあるプラスボタンをクリック。
④
Bundle IDとDescriptionを記入する。
MusicKitにチェックを入れる。
Continueをクリック。
⑤
プロジェクトのBundle IDを④で設定したIDと同値にする
2. クライアントアプリがユーザーの音楽メディアライブラリへのアクセスを要求する理由をユーザーに伝えるためのメッセージを設定する
info.plistに以下のkey-valueを追加する
Name | Type |
---|---|
Privacy - Media Library Usage Description | String |
<key>NSAppleMusicUsageDescription</key>
<string>楽曲再生のために音楽ライブラリにアクセスする必要があります。</string>
ここでは以下のAPIを使ってみようと思います
-
MusicLibraryRequest
ユーザーのApple Musicのライブラリ情報(アルバムや曲やプレイリストなど)をリクエストする
-
MusicPersonalRecommendationsRequest
ユーザーのApple Musicのライブラリ情報や再生履歴を元にオススメの音楽をリクエストする
-
ApplicationMusicPlayer
音楽を再生することができるオブジェクトで、他の音楽再生アプリの状態に影響を与えない。
音楽再生機能を持つもう一つのSystemMusicPlayerというオブジェクトがあるが、こちらは他の音楽再生アプリの状態に影響を与えるので、今回はApplicationMusicPlayerを利用する。
MusicLibraryRequestを使ってApple Music内のライブラリにアクセスする
MusicKitのAPIを呼び出すモジュール。
MusicLibraryRequest<MusicItemType>
のresponse()
を呼び出すとMusicLibraryResponse<MusicItemType>
が返却されます。
MusicLibraryResponse<MusicItemType>
はitems
プロパティを持っていて、ここに音楽情報のコレクションであるMusicItemCollection<MusicItemType>
が格納されています。
MusicItemType
に準拠しているオブジェクトにはSong
やAlbum
やPlaylist
などがあり、それぞれMusicLibraryRequest
に指定することができます。
今回は曲一覧を取得したいのでSong
を指定します。
import Foundation
import MusicKit
protocol MusicService {
func fetchSongs() async throws -> MusicItemCollection<Song>
}
class MusicServiceImpl: MusicService {
func fetchSongs() async throws -> MusicItemCollection<Song> {
do {
let response = try await MusicLibraryRequest<Song>().response()
return response.items
} catch {
handleError(error, context: "Fetching songs failed")
throw error
}
}
}
MusicServiceのAPIを呼び出し、返却された楽曲情報をsongs: MusicItemCollection<Song>
変数に格納し、Viewに公開します。
MusicKitを使用して音楽情報にアクセスするには、最初にMusicAuthorization.request()
でアプリがユーザーの音楽情報にアクセスするためのリクエストを実行します。
import Foundation
import MusicKit
class MusicViewModel: ObservableObject {
private let musicService: MusicService
@Published var songs: MusicItemCollection<Song> = []
@Published var authorizationStatus: MusicAuthorization.Status = .notDetermined
init(musicService: MusicService) {
self.musicService = musicService
}
func authorize() async {
let status = await MusicAuthorization.request()
DispatchQueue.main.async { [self] in
authorizationStatus = status
}
}
func fetchSongs() async throws {
guard authorizationStatus == .authorized else {
print("not authorized")
return
}
do {
let result = try await musicService.fetchSongs()
DispatchQueue.main.async {
self.songs = result
}
} catch {
print(error)
}
}
}
MusicViewModelで保持しているsongs: MusicItemCollection<Song>
にアクセスしてartwork
やtitle
やartistName
を表示します。
import SwiftUI
import MusicKit
struct ContentView: View {
@StateObject private var viewModel = MusicViewModel(musicService: MusicServiceImpl())
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Text("ライブラリの曲一覧を取得")
.font(.headline)
ScrollView(.horizontal) {
LazyHStack(alignment: .top) {
if viewModel.songs.isEmpty {
Text("Empty Playlist")
} else {
ForEach(Array(viewModel.songs)) { song in
VStack(alignment: .leading) {
if let artwork = song.artwork {
ArtworkImage(artwork, width: 100, height: 100)
} else {
Image(systemName: "music.note")
.frame(width: 100, height: 100, alignment: .leading)
}
VStack(alignment: .leading) {
Text(song.title)
.font(.headline)
.frame(width: 100)
.lineLimit(1)
Text(song.artistName)
.font(.caption)
}
}
.padding(.horizontal, 5)
}
}
}
.padding()
}
}
.onAppear() {
Task {
await viewModel.authorize()
try await viewModel.fetchSongs()
}
}
}
}
}
MusicPersonalRecommendationsRequestを使ってオススメの音楽情報をリクエストする
MusicPersonalRecommendationsRequestはユーザーのApple Musicのライブラリ情報や再生履歴を元にパーソナライズされたオススメの音楽をリクエストすることができるAPIです。
import Foundation
import MusicKit
protocol MusicService {
func fetchRecommendations() async throws -> MusicItemCollection<MusicPersonalRecommendation>
}
class MusicServiceImpl: MusicService {
func fetchRecommendations() async throws -> MusicItemCollection<MusicPersonalRecommendation> {
do {
let response = try await MusicPersonalRecommendationsRequest().response()
return response.recommendations
} catch {
handleError(error, context: "Fetching recommendations failed")
throw error
}
}
}
import Foundation
import MusicKit
class MusicViewModel: ObservableObject {
private let musicService: MusicService
@Published var authorizationStatus: MusicAuthorization.Status = .notDetermined
@Published var recomendatedAlbums: [AlbumEnity] = []
@Published var recomendatedPlaylists: [PlaylistEnity] = []
@Published var recomendatedStations: [StationEnity] = []
init(musicService: MusicService) {
self.musicService = musicService
}
func authorize() async {
let status = await MusicAuthorization.request()
DispatchQueue.main.async { [self] in
authorizationStatus = status
handleAuthorizationStatus(status: status)
}
}
func fetchRecomendations() async throws {
guard authorizationStatus == .authorized else {
print("not authorized")
return
}
do {
let recomendations = try await musicService.fetchRecommendations()
for recomendation in recomendations {
for item in recomendation.items {
switch item {
// MusicItemCollection<Album>のコレクションからAlbumを抽出
case .album(let album): do {
self.recomendatedAlbums.append(
AlbumEnity(
id: album.id,
title: album.title,
artistName: album.artistName,
artwork: album.artwork
)
)
}
// MusicItemCollection<Playlist>のコレクションからPlaylistを抽出
case .playlist(let playlist): do {
self.recomendatedPlaylists.append(
PlaylistEnity(
id: playlist.id,
title: playlist.name,
artwork: playlist.artwork
)
)
}
// MusicItemCollection<Station>のコレクションからStationを抽出
case .station(let station): do {
self.recomendatedStations.append(
StationEnity(
id: station.id,
title: station.name,
artwork: station.artwork
)
)
}
@unknown default:
return
}
}
}
}
}
}
struct AlbumEnity: Identifiable {
var id: MusicItemID
var title: String
var artistName: String
var artwork: Artwork?
}
struct PlaylistEnity: Identifiable {
var id: MusicItemID
var title: String
var artwork: Artwork?
}
struct StationEnity: Identifiable {
var id: MusicItemID
var title: String
var artwork: Artwork?
}
以下のUIコードはAlbum
のみなのでPlaylist
やStation
を表示したい場合は読み替えてください。
import SwiftUI
import MusicKit
struct ContentView: View {
@StateObject private var viewModel = MusicViewModel(musicService: MusicServiceImpl())
var body: some View {
ZStack {
ScrollView {
VStack(alignment: .leading) {
Text("オススメのアルバム一覧を取得")
.font(.headline)
ScrollView(.horizontal) {
LazyHStack(alignment: .top) {
if viewModel.recomendatedAlbums.isEmpty {
Text("Empty Playlist")
} else {
ForEach(viewModel.recomendatedAlbums) { recommendation in
VStack(alignment: .leading) {
if let artwork = recommendation.artwork {
ArtworkImage(artwork, width: 100, height: 100)
} else {
Image(systemName: "music.note")
.frame(width: 100, height: 100, alignment: .leading)
}
VStack(alignment: .leading) {
Text(recommendation.title)
.font(.headline)
.frame(width: 100)
.lineLimit(1)
}
}
.padding(.horizontal, 5)
}
}
}
.padding()
}
}
.onAppear() {
Task {
await viewModel.authorize()
try await viewModel.fetchRecomendations()
}
}
}
}
}
}
ApplicationMusicPlayerを使って音楽を再生する
ApplicationMusicPlayer.shared.queue
に再生する音楽情報をリストで保持していて、ここに追加された音楽情報を順番に再生する仕組みです。
import Foundation
import MusicKit
protocol MusicService {
func playback(song: Song) async throws
func restartPlayback() async throws
func pause()
}
class MusicServiceImpl: MusicService {
let player = ApplicationMusicPlayer.shared
func playback(song: Song) async throws {
do {
player.queue = [song]
try await player.play()
} catch {
handleError(error, context: "Playing song '\(song.title)' failed")
throw error
}
}
func restartPlayback() async throws{
do {
try await player.play()
} catch {
handleError(error, context: "restart song failed")
throw error
}
}
func pause() {
player.pause()
}
}
import Foundation
import MusicKit
class MusicViewModel: ObservableObject {
private let musicService: MusicService
@Published var isPlaying: Bool = false
@Published var playingSong: Song?
init(musicService: MusicService) {
self.musicService = musicService
}
func playback(song: Song) async throws{
DispatchQueue.main.async {
self.isPlaying = true
self.playingSong = song
}
try await musicService.playback(song: song)
}
func restartPlayback() async throws{
DispatchQueue.main.async {
self.isPlaying = true
}
try await musicService.restartPlayback()
}
func pause() {
isPlaying = false
musicService.pause()
}
}
import SwiftUI
import MusicKit
struct ContentView: View {
@StateObject private var viewModel = MusicViewModel(musicService: MusicServiceImpl())
var body: some View {
ZStack {
ScrollView {
VStack(alignment: .leading) {
// MusicLibraryRequestのレスポンスから取得した音楽情報を表示して、タップした契機で再生処理を発火させる
Text("ライブラリの曲一覧を取得")
.font(.headline)
ScrollView(.horizontal) {
LazyHStack(alignment: .top) {
if viewModel.songs.isEmpty {
Text("Empty Playlist")
} else {
ForEach(Array(viewModel.songs)) { song in
VStack(alignment: .leading) {
if let artwork = song.artwork {
ArtworkImage(artwork, width: 100, height: 100)
} else {
Image(systemName: "music.note")
.frame(width: 100, height: 100, alignment: .leading)
}
VStack(alignment: .leading) {
Text(song.title)
.font(.headline)
.frame(width: 100)
.lineLimit(1)
Text(song.artistName)
.font(.caption)
}
}
.padding(.horizontal, 5)
.onTapGesture {
Task {
// 音楽再生処理を開始
try await viewModel.playback(song: song)
}
}
}
}
}
.padding()
}
}
.onAppear() {
Task {
await viewModel.authorize()
try await viewModel.fetchSongs()
}
}
}
}
// 再生と一時停止を制御する
PlayingBar(
playingSong: $viewModel.playingSong,
isPlaying: $viewModel.isPlaying,
restartPlayback: { try await viewModel.restartPlayback() },
pausePlayback: { viewModel.pause() }
)
}
}
struct PlayingBar: View {
@Binding var playingSong: Song?
@Binding var isPlaying: Bool
var restartPlayback: () async throws -> Void
var pausePlayback: () -> Void
var body: some View {
ZStack {
Color.green
HStack {
Text(playingSong?.title ?? "再生中の曲はありません")
Spacer()
Image(systemName: isPlaying ? "pause.fill" : "play.fill")
.onTapGesture {
if isPlaying {
pausePlayback()
} else {
guard let _ = playingSong else { return }
Task {
try await restartPlayback()
}
}
}
}
.padding()
.background(Color.green)
}
.frame(height: 70)
}
}