MusicKit Integration (Apple Music)
Time cost: 5-10 minutes
Key Insight
MusicKit's ApplicationMusicPlayer automatically publishes to MPNowPlayingInfoCenter. You don't need to manually update Now Playing info when playing Apple Music content.
What's Automatic
When using ApplicationMusicPlayer :
-
Track title, artist, album
-
Artwork (Apple's album art)
-
Duration and elapsed time
-
Playback rate (playing/paused state)
The system handles all MPNowPlayingInfoCenter updates for you.
What's NOT Automatic
-
Custom metadata (chapter markers, custom artist notes)
-
Remote command customization beyond standard controls
-
Mixing MusicKit content with your own content
Subscription and Authorization
Check Music Authorization
import MusicKit
func requestMusicAccess() async -> Bool { let status = await MusicAuthorization.request() return status == .authorized }
// Check current status without prompting let currentStatus = MusicAuthorization.currentStatus // .authorized, .denied, .notDetermined, .restricted
Check Apple Music Subscription
func checkSubscription() async -> Bool { do { let subscription = try await MusicSubscription.current return subscription.canPlayCatalogContent } catch { return false } }
// Observe subscription changes func observeSubscription() { Task { for await subscription in MusicSubscription.subscriptionUpdates { if subscription.canPlayCatalogContent { // Full Apple Music access } else if subscription.canBecomeSubscriber { // Show subscription offer showSubscriptionOffer() } } } }
Subscription Offer Sheet
import MusicKit import StoreKit
// Present Apple Music subscription offer MusicSubscriptionOffer.Options( messageIdentifier: .playMusic, itemID: song.id )
// In SwiftUI .musicSubscriptionOffer(isPresented: $showOffer, options: offerOptions)
Graceful Fallback Without Subscription
@MainActor class MusicPlayer: ObservableObject { @Published var canPlay = false
func handlePlayRequest(song: Song) async {
let authorized = await requestMusicAccess()
guard authorized else {
showAuthorizationDeniedAlert()
return
}
do {
let subscription = try await MusicSubscription.current
if subscription.canPlayCatalogContent {
// Full playback
try await play(song: song)
} else {
// Preview only (30-second clips)
if let previewURL = song.previewAssets?.first?.url {
playPreview(url: previewURL)
}
}
} catch {
handleError(error)
}
}
}
Playback
Basic Playback
import MusicKit
@MainActor class MusicKitPlayer { private let player = ApplicationMusicPlayer.shared
func play(song: Song) async throws {
// ✅ Just play - MPNowPlayingInfoCenter updates automatically
player.queue = [song]
try await player.play()
// ❌ DO NOT manually set nowPlayingInfo here
// MPNowPlayingInfoCenter.default().nowPlayingInfo = [...] // WRONG!
}
func pause() {
player.pause()
}
func stop() {
player.stop()
}
}
Observing Playback State
@MainActor class PlayerViewModel: ObservableObject { private let player = ApplicationMusicPlayer.shared @Published var isPlaying = false @Published var currentEntry: ApplicationMusicPlayer.Queue.Entry? @Published var playbackTime: TimeInterval = 0
func observeState() {
// Observe playback status
Task {
for await state in player.state.objectWillChange.values {
isPlaying = player.state.playbackStatus == .playing
}
}
// Observe current entry (track changes)
Task {
for await queue in player.queue.objectWillChange.values {
currentEntry = player.queue.currentEntry
}
}
}
}
Queue Management
Setting the Queue
let player = ApplicationMusicPlayer.shared
// Single song player.queue = [song]
// Album player.queue = ApplicationMusicPlayer.Queue(album: album)
// Playlist player.queue = ApplicationMusicPlayer.Queue(playlist: playlist)
// Multiple items player.queue = ApplicationMusicPlayer.Queue(for: [song1, song2, song3])
// Start at specific item player.queue = ApplicationMusicPlayer.Queue(for: songs, startingAt: songs[2])
Queue Operations
// Skip to next try await player.skipToNextEntry()
// Skip to previous try await player.skipToPreviousEntry()
// Restart current track player.restartCurrentEntry()
// Append to queue try await player.queue.insert(song, position: .afterCurrentEntry) try await player.queue.insert(song, position: .tail) // End of queue
// Shuffle and repeat player.state.shuffleMode = .songs // .off, .songs player.state.repeatMode = .all // .none, .one, .all
Observing Queue Changes
// Current track info if let entry = player.queue.currentEntry { let title = entry.title let subtitle = entry.subtitle // Artist name let artwork = entry.artwork // Artwork for display
// Get full Song object if needed
if case .song(let song) = entry.item {
let albumTitle = song.albumTitle
}
}
Hybrid Apps (Own Content + Apple Music)
If your app plays both Apple Music and your own content:
import MusicKit
@MainActor class HybridPlayer { private let musicKitPlayer = ApplicationMusicPlayer.shared private var avPlayer: AVPlayer? private var currentSource: ContentSource = .none
enum ContentSource {
case none
case appleMusic // MusicKit handles Now Playing
case ownContent // We handle Now Playing
}
func playAppleMusicSong(_ song: Song) async throws {
// Switch to MusicKit
avPlayer?.pause()
currentSource = .appleMusic
musicKitPlayer.queue = [song]
try await musicKitPlayer.play()
// ✅ MusicKit handles Now Playing automatically
}
func playOwnContent(_ url: URL) {
// Switch to AVPlayer
musicKitPlayer.pause()
currentSource = .ownContent
avPlayer = AVPlayer(url: url)
avPlayer?.play()
// ✅ Manually update Now Playing (see axiom-now-playing)
updateNowPlayingForOwnContent()
}
private func updateNowPlayingForOwnContent() {
var nowPlayingInfo = [String: Any]()
nowPlayingInfo[MPMediaItemPropertyTitle] = "My Track"
// ... rest of manual setup
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
}
Common Mistake
// ❌ WRONG - Overwrites MusicKit's automatic Now Playing data func playAppleMusicSong(_ song: Song) async throws { try await ApplicationMusicPlayer.shared.play()
// ❌ This clears MusicKit's Now Playing info!
var nowPlayingInfo = [String: Any]()
nowPlayingInfo[MPMediaItemPropertyTitle] = song.title
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
// ✅ CORRECT - Let MusicKit handle it func playAppleMusicSong(_ song: Song) async throws { try await ApplicationMusicPlayer.shared.play() // That's it! MusicKit publishes Now Playing automatically. }
When to Use Manual Updates with MusicKit
Only override MPNowPlayingInfoCenter if:
-
You're mixing in additional metadata (e.g., podcast chapter markers)
-
You're displaying custom content alongside Apple Music
-
You have a specific reason to replace MusicKit's automatic behavior
Default: Let MusicKit manage Now Playing automatically.
Resources
Docs: /musickit, /musickit/applicationmusicplayer, /musickit/musicsubscription
Skills: axiom-now-playing, axiom-now-playing-carplay