Agent skill
axiom-now-playing-musickit
MusicKit Now Playing integration patterns. Use when playing Apple Music content with ApplicationMusicPlayer and understanding automatic vs manual Now Playing info updates.
Install this agent skill to your Project
npx add-skill https://github.com/CharlesWiltgen/Axiom/tree/main/axiom-codex/skills/axiom-now-playing-musickit
SKILL.md
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
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
axiom-mapkit-diag
MapKit troubleshooting — annotations not appearing, region jumping, clustering not working, search failures, overlay rendering issues, user location problems
axiom-core-location
Use for Core Location implementation patterns - authorization strategy, monitoring strategy, accuracy selection, background location
axiom-energy
Use when app drains battery, device gets hot, users report energy issues, or auditing power consumption - systematic Power Profiler diagnosis, subsystem identification (CPU/GPU/Network/Location/Display), anti-pattern fixes for iOS/iPadOS
axiom-audit-storage
Use when the user mentions file storage issues, data loss, backup bloat, or asks to audit storage usage.
axiom-app-store-connect-ref
Use when navigating App Store Connect to find crash data, read TestFlight feedback, interpret metrics dashboards, or export diagnostic logs. Covers crash-free rates, dSYM symbolication, termination types, MetricKit.
axiom-testflight-triage
Use when ANY beta tester reports a crash, ANY crash appears in Organizer or App Store Connect, crash logs need symbolication, app was killed without crash report, or you need to triage TestFlight feedback
Didn't find tool you were looking for?