| name | axiom-now-playing |
| description | Use when Now Playing metadata doesn't appear on Lock Screen/Control Center, remote commands (play/pause/skip) don't respond, artwork is missing/wrong/flickering, or playback state is out of sync - provides systematic diagnosis, correct patterns, and professional push-back for audio/video apps on iOS 18+ |
| license | MIT |
| compatibility | iOS 18+, iPadOS 18+, CarPlay (iOS 14+) |
| metadata | {"version":"1.1.0"} |
Now Playing Integration Guide
Purpose: Prevent the 4 most common Now Playing issues on iOS 18+: info not appearing, commands not working, artwork problems, and state sync issues
Swift Version: Swift 6.0+
iOS Version: iOS 18+
Xcode: Xcode 16+
Core Philosophy
"Now Playing eligibility requires THREE things working together: AVAudioSession activation, remote command handlers, and metadata publishing. Missing ANY of these silently breaks the entire system. 90% of Now Playing issues stem from incorrect activation order or missing command handlers, not API bugs."
Key Insight from WWDC 2022/110338: Apps must meet two system heuristics:
- Register handlers for at least one remote command
- Configure AVAudioSession with a non-mixable category
When to Use This Skill
✅ Use this skill when:
- Now Playing info doesn't appear on Lock Screen or Control Center
- Play/pause/skip buttons are grayed out or don't respond
- Album artwork is missing, wrong, or flickers between images
- Control Center shows "Playing" when app is paused, or vice versa
- Apple Music or other apps "steal" Now Playing status
- Implementing Now Playing for the first time
- Debugging Now Playing issues in existing implementation
- Integrating CarPlay Now Playing (covered in Pattern 6)
- Working with MusicKit/Apple Music content (covered in Pattern 7)
iOS 26 Note
iOS 26 introduces Liquid Glass visual design for Lock Screen and Control Center Now Playing widgets. This is automatic system behavior — no code changes required. The patterns in this skill remain valid for iOS 26.
❌ Do NOT use this skill for:
- Background audio configuration details (see AVFoundation skill)
Related Skills
- swift-concurrency - For @MainActor patterns, weak self in closures, async artwork loading
- memory-debugging - For retain cycles in command handlers
- avfoundation-ref - For AVAudioSession configuration details
Red Flags / Anti-Patterns
If you see ANY of these, suspect Now Playing misconfiguration:
- Info appears briefly then disappears (AVAudioSession deactivated)
- Commands work in simulator but not on device (simulator has different audio stack)
- Artwork shows placeholder then updates (race condition, not necessarily wrong)
- Artwork never appears (format/size issue or MPMediaItemArtwork block returning nil)
- Play/pause state incorrect after backgrounding (not updating on playback rate changes)
- Another app "steals" Now Playing (didn't meet eligibility requirements)
playbackState property doesn't update (iOS doesn't have playbackState, macOS only!)
FORBIDDEN Assumptions:
- "Just set nowPlayingInfo and it works" - Must have AVAudioSession + command handlers
- "playbackState controls Control Center" - iOS ignores playbackState, uses playbackRate
- "Artwork just needs an image" - Needs proper MPMediaItemArtwork with size handler
- "Commands enable themselves" - Must add target AND set isEnabled = true
- "Update elapsed time every second" - System infers from rate, causes jitter
Mandatory First Steps (Pre-Diagnosis)
Run this code to understand current state before debugging:
let session = AVAudioSession.sharedInstance()
print("Category: \(session.category.rawValue)")
print("Mode: \(session.mode.rawValue)")
print("Options: \(session.categoryOptions)")
print("Is active: \(try? session.setActive(true))")
let commandCenter = MPRemoteCommandCenter.shared()
print("Play enabled: \(commandCenter.playCommand.isEnabled)")
print("Pause enabled: \(commandCenter.pauseCommand.isEnabled)")
if let info = MPNowPlayingInfoCenter.default().nowPlayingInfo {
print("Title: \(info[MPMediaItemPropertyTitle] ?? "nil")")
print("Artwork: \(info[MPMediaItemPropertyArtwork] != nil)")
print()
()
()
} {
()
}
What this tells you:
| Observation | Diagnosis | Pattern |
|---|
| Category is .ambient or has .mixWithOthers | Won't become Now Playing app | Pattern 1 |
| No commands have targets | System ignores app | Pattern 2 |
| Commands have targets but isEnabled = false | UI grayed out | Pattern 2 |
| Artwork is nil | MPMediaItemArtwork block returning nil | Pattern 3 |
| playbackRate is 0.0 when playing | Control Center shows paused | Pattern 4 |
| Background mode "audio" not in Info.plist | Info disappears on lock | Pattern 1 |
Decision Tree
Now Playing not working?
├─ Info never appears at all?
│ ├─ AVAudioSession category .ambient or .mixWithOthers?
│ │ └─ Pattern 1a (Wrong Category)
│ ├─ No remote command handlers registered?
│ │ └─ Pattern 2a (Missing Handlers)
│ ├─ Background mode "audio" not in Info.plist?
│ │ └─ Pattern 1b (Background Mode)
│ └─ AVAudioSession.setActive(true) never called?
│ └─ Pattern 1c (Not Activated)
│
├─ Info appears briefly, then disappears?
│ ├─ On lock screen specifically?
│ │ ├─ AVAudioSession deactivated too early?
│ │ │ └─ Pattern 1d (Early Deactivation)
│ │ └─ App suspended (no background mode)?
│ │ └─ Pattern 1b (Background Mode)
│ └─ When switching apps?
│ └─ Another app claiming Now Playing → Pattern 5
│
├─ Commands not responding?
│ ├─ Buttons grayed out (disabled)?
│ │ └─ command.isEnabled = false → Pattern 2b
│ ├─ Buttons visible but no response?
│ │ ├─ Handler not returning .success?
│ │ │ └─ Pattern 2c (Handler Return)
│ │ └─ Using wrong command center (session vs shared)?
│ │ └─ Pattern 2d (Command Center)
│ └─ Skip forward/backward not showing?
│ └─ preferredIntervals not set → Pattern 2e
│
├─ Artwork problems?
│ ├─ Never appears?
│ │ ├─ MPMediaItemArtwork block returning nil?
│ │ │ └─ Pattern 3a (Artwork Block)
│ │ └─ Image format/size invalid?
│ │ └─ Pattern 3b (Image Format)
│ ├─ Wrong artwork showing?
│ │ └─ Race condition between sources → Pattern 3c
│ └─ Artwork flickering?
│ └─ Multiple updates in rapid succession → Pattern 3d
│
├─ State sync issues?
│ ├─ Shows "Playing" when paused?
│ │ └─ playbackRate not updated → Pattern 4a
│ ├─ Progress bar stuck or jumping?
│ │ └─ elapsedTime not updated at right moments → Pattern 4b
│ └─ Duration wrong?
│ └─ Not setting playbackDuration → Pattern 4c
│
├─ CarPlay specific issues?
│ ├─ App doesn't appear in CarPlay at all?
│ │ └─ Missing entitlement → Pattern 6 (Add com.apple.developer.carplay-audio)
│ ├─ Now Playing blank in CarPlay but works on iOS?
│ │ └─ Same root cause as iOS → Check Patterns 1-4
│ ├─ Custom buttons don't appear in CarPlay?
│ │ └─ Wrong configuration timing → Pattern 6 (Configure at templateApplicationScene)
│ └─ Works on device but not CarPlay simulator?
│ └─ Debugger interference → Pattern 6 (Run without debugger)
│
└─ Using MusicKit (ApplicationMusicPlayer)?
├─ Now Playing shows wrong info?
│ └─ Overwriting automatic data → Pattern 7 (Don't set nowPlayingInfo manually)
└─ Mixing MusicKit + own content?
└─ Hybrid approach needed → Pattern 7 (Switch between players)
Pattern 1: AVAudioSession Configuration (Info Not Appearing)
Time cost: 10-15 minutes
Symptom
- Now Playing info never appears on Lock Screen
- Info appears briefly then disappears on lock
- Works in foreground, disappears in background
BAD Code
class PlayerService {
func setupAudioSession() throws {
try AVAudioSession.sharedInstance().setCategory(
.playback,
options: .mixWithOthers
)
}
func play() {
player.play()
updateNowPlaying()
}
}
GOOD Code
class PlayerService {
func setupAudioSession() throws {
try AVAudioSession.sharedInstance().setCategory(
.playback,
mode: .default,
options: []
)
}
func play() async throws {
try AVAudioSession.sharedInstance().setActive(true)
player.play()
updateNowPlaying()
}
func stop() async throws {
player.pause()
try AVAudioSession.sharedInstance().setActive(
false,
options: .notifyOthersOnDeactivation
)
}
}
Info.plist Requirement
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
Verification
- Lock screen shows Now Playing controls
- Info persists when app backgrounded
- Survives app switch (unless another app plays)
Pattern 2: Remote Command Registration (Commands Not Working)
Time cost: 15-20 minutes
Symptom
- Play/pause buttons grayed out
- Buttons visible but tapping does nothing
- Skip buttons don't appear
- Commands work once then stop
BAD Code
class PlayerService {
func setupCommands() {
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.addTarget { _ in
self.player.play()
return .success
}
commandCenter.skipForwardCommand.addTarget { _ in
return .success
}
}
}
GOOD Code
@MainActor
class PlayerService {
private var commandTargets: [Any] = []
func setupCommands() {
let commandCenter = MPRemoteCommandCenter.shared()
let playTarget = commandCenter.playCommand.addTarget { [weak self] _ in
self?.player.play()
self?.updateNowPlayingPlaybackState(isPlaying: true)
return .success
}
commandCenter.playCommand.isEnabled = true
commandTargets.append(playTarget)
let pauseTarget = commandCenter.pauseCommand.addTarget { [weak self] _ in
self?.player.pause()
self?.updateNowPlayingPlaybackState(isPlaying: false)
return .success
}
commandCenter.pauseCommand.isEnabled = true
commandTargets.append(pauseTarget)
commandCenter.skipForwardCommand.preferredIntervals []
skipForwardTarget commandCenter.skipForwardCommand.addTarget { [ ] event
skipEvent event {
.commandFailed
}
.skip(by: skipEvent.interval)
.success
}
commandCenter.skipForwardCommand.isEnabled
commandTargets.append(skipForwardTarget)
commandCenter.skipBackwardCommand.preferredIntervals []
skipBackwardTarget commandCenter.skipBackwardCommand.addTarget { [ ] event
skipEvent event {
.commandFailed
}
.skip(by: skipEvent.interval)
.success
}
commandCenter.skipBackwardCommand.isEnabled
commandTargets.append(skipBackwardTarget)
}
() {
commandCenter .shared()
commandCenter.playCommand.removeTarget()
commandCenter.pauseCommand.removeTarget()
commandCenter.skipForwardCommand.removeTarget()
commandCenter.skipBackwardCommand.removeTarget()
commandTargets.removeAll()
}
{
teardownCommands()
}
}
Verification
- Buttons not grayed out in Control Center
- Tapping play/pause actually plays/pauses
- Skip buttons show with correct interval (15s)
Pattern 3: Artwork Configuration (Artwork Problems)
Time cost: 15-25 minutes
Symptom
- Artwork never appears (generic placeholder)
- Wrong artwork for current track
- Artwork flickers between images
- Artwork appears then disappears
BAD Code
func updateNowPlaying() {
var nowPlayingInfo = [String: Any]()
nowPlayingInfo[MPMediaItemPropertyTitle] = track.title
nowPlayingInfo[MPMediaItemPropertyArtwork] = image
let artwork = MPMediaItemArtwork(boundsSize: image.size) { _ in
return self.cachedImage
}
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
func loadArtwork(from url: URL) {
loadImage(url) { image in
self.updateNowPlayingArtwork(image)
}
loadCachedImage(url) { image in
self.updateNowPlayingArtwork(image)
}
}
GOOD Code
@MainActor
class NowPlayingService {
private var currentArtworkURL: URL?
func updateNowPlayingArtwork(_ image: UIImage, for trackURL: URL) {
guard trackURL == currentArtworkURL else { return }
let artwork = MPMediaItemArtwork(boundsSize: image.size) { [image] requestedSize in
return image
}
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
func ( : ) {
currentArtworkURL track.artworkURL
embedded extractEmbeddedArtwork(track.fileURL) {
updateNowPlayingArtwork(embedded, for: track.artworkURL)
}
cached loadFromCache(track.artworkURL) {
updateNowPlayingArtwork(cached, for: track.artworkURL)
}
remote downloadImage(track.artworkURL) {
updateNowPlayingArtwork(remote, for: track.artworkURL)
}
}
}
Why value capture, not nonisolated(unsafe): The closure passed to MPMediaItemArtwork may be called by the system from any thread. Under Swift 6 strict concurrency, accessing @MainActor-isolated stored properties from this closure would cause a compile error. Capturing the image value directly is cleaner than using nonisolated(unsafe) because UIImage is immutable and thread-safe for reads.
Artwork Size Guidelines
- Lock Screen: 300x300 points (600x600 @2x, 900x900 @3x)
- Control Center: Various sizes
- Best practice: Provide image at least 600x600 pixels
Verification
- Artwork appears on Lock Screen
- Correct artwork for current track
- No flickering when track changes
- Artwork persists after backgrounding
Pattern 4: Playback State Synchronization (State Sync Issues)
Time cost: 10-20 minutes
Symptom
- Control Center shows "Playing" when actually paused
- Progress bar doesn't move or jumps unexpectedly
- Duration shows wrong value
- Scrubbing doesn't work correctly
BAD Code
func updatePlaybackState(isPlaying: Bool) {
MPNowPlayingInfoCenter.default().playbackState = isPlaying ? .playing : .paused
}
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
var info = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = self.player.currentTime().seconds
MPNowPlayingInfoCenter.default().nowPlayingInfo = info
}
func updateTitle() {
var info = [String: Any]()
info[MPMediaItemPropertyTitle] = track.title
MPNowPlayingInfoCenter.default().nowPlayingInfo = info
}
GOOD Code
@MainActor
class NowPlayingService {
func playbackStarted(track: Track, player: AVPlayer) {
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
nowPlayingInfo[MPMediaItemPropertyTitle] = track.title
nowPlayingInfo[MPMediaItemPropertyArtist] = track.artist
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = track.album
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = player.currentItem?.duration.seconds ?? 0
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player.currentTime().seconds
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1.0
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
func playbackPaused(player: AVPlayer) {
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo [:]
nowPlayingInfo[] player.currentTime().seconds
nowPlayingInfo[]
.default().nowPlayingInfo nowPlayingInfo
}
( : , : ) {
nowPlayingInfo .default().nowPlayingInfo [:]
nowPlayingInfo[] time.seconds
.default().nowPlayingInfo nowPlayingInfo
}
( : , : ) {
nowPlayingInfo [: ]()
nowPlayingInfo[] newTrack.title
nowPlayingInfo[] newTrack.artist
nowPlayingInfo[] newTrack.album
nowPlayingInfo[] player.currentItem.duration.seconds
nowPlayingInfo[]
nowPlayingInfo[] player.rate
.default().nowPlayingInfo nowPlayingInfo
{
loadArtwork(for: newTrack)
}
}
}
When to Update Now Playing Info
| Event | What to Update |
|---|
| Playback starts | All metadata + elapsed=current + rate=1.0 |
| Playback pauses | elapsed=current + rate=0.0 |
| User seeks | elapsed=newPosition (keep rate) |
| Track changes | All metadata (new track) |
| Playback rate changes (2x, 0.5x) | rate=newRate |
DO NOT Update
- On a timer (system infers from elapsed + rate + timestamp)
- Elapsed time continuously (causes jitter)
- Partial dictionaries (loses other values)
Pattern 5: MPNowPlayingSession (iOS 16+ Recommended Approach)
Time cost: 20-30 minutes
When to Use MPNowPlayingSession
- iOS 16+ (available since iOS 16, previously tvOS only)
- Using AVPlayer for playback
- Want automatic publishing of playback state
- Multiple players (Picture-in-Picture scenarios)
BAD Code (Manual Approach - More Error-Prone)
class OldStylePlayer {
func play() {
player.play()
updateNowPlayingElapsed()
updateNowPlayingRate()
}
}
GOOD Code (MPNowPlayingSession)
@MainActor
class ModernPlayerService {
private var player: AVPlayer
private var session: MPNowPlayingSession?
init() {
player = AVPlayer()
setupSession()
}
func setupSession() {
session = MPNowPlayingSession(players: [player])
session?.automaticallyPublishNowPlayingInfo = true
session?.remoteCommandCenter.playCommand.addTarget { [weak self] _ in
self?.player.play()
return .success
}
session?.remoteCommandCenter.playCommand.isEnabled = true
session?.remoteCommandCenter.pauseCommand.addTarget { [weak self] _ in
self?.player.pause()
.success
}
session.remoteCommandCenter.pauseCommand.isEnabled
session.becomeActiveIfPossible { success
()
}
}
(: ) {
item (url: track.url)
item.nowPlayingInfo [
: track.title,
: track.artist,
: createArtwork(for: track)
]
player.replaceCurrentItem(with: item)
player.play()
}
}
Multiple Sessions (Picture-in-Picture)
class MultiPlayerService {
var mainSession: MPNowPlayingSession
var pipSession: MPNowPlayingSession
func pipDidExpand() {
pipSession.becomeActiveIfPossible { success in
}
}
func pipDidMinimize() {
mainSession.becomeActiveIfPossible { success in
}
}
}
Critical Gotcha
When using MPNowPlayingSession: Use session.remoteCommandCenter, NOT MPRemoteCommandCenter.shared()
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.addTarget { _ in }
session.remoteCommandCenter.playCommand.addTarget { _ in }
Pattern 6: CarPlay Integration
For CarPlay-specific integration patterns, invoke /skill axiom-now-playing-carplay.
Key insight: CarPlay uses the SAME MPNowPlayingInfoCenter and MPRemoteCommandCenter as iOS. If your Now Playing works on iOS, it works in CarPlay with zero additional code.
Pattern 7: MusicKit Integration (Apple Music)
For MusicKit-specific integration patterns and hybrid app examples, invoke /skill axiom-now-playing-musickit.
Key insight: MusicKit's ApplicationMusicPlayer automatically publishes to MPNowPlayingInfoCenter. You don't need to manually update Now Playing info when playing Apple Music content.
Pressure Scenarios
Scenario 1: Apple Music Keeps Taking Over (24-Hour Launch Deadline)
Situation
- App launching tomorrow
- QA reports: "Now Playing works, but when user opens Apple Music then returns to our app, our controls disappear"
- Product manager: "This is a blocker, users will think our app is broken"
- You're 2 hours from code freeze
Rationalization Traps (DO NOT)
- "Just tell users not to use Apple Music" - Unacceptable UX, will get 1-star reviews
- "Force our app to always be Now Playing" - Impossible, system controls eligibility
- "File a bug with Apple" - Won't help before launch
Root Cause
Your app loses eligibility because:
- Using
.mixWithOthers option (allows other apps to play simultaneously)
- Not calling
becomeActiveIfPossible() when returning to foreground
- AVAudioSession deactivated when backgrounded
Systematic Fix (30 minutes)
try AVAudioSession.sharedInstance().setCategory(.playback, options: [])
NotificationCenter.default.addObserver(
forName: UIApplication.willEnterForegroundNotification,
object: nil,
queue: .main
) { [weak self] _ in
guard self?.isPlaying == true else { return }
do {
try AVAudioSession.sharedInstance().setActive(true)
self?.session?.becomeActiveIfPossible { _ in }
} catch {
print("Failed to reactivate audio session: \(error)")
}
}
NotificationCenter.default.addObserver(
forName: AVAudioSession.interruptionNotification,
object: nil,
queue: .main
) { [weak self] notification in
guard let info = notification.userInfo,
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = .(rawValue: typeValue) {
}
type .ended {
.sharedInstance().setActive()
.session.becomeActiveIfPossible { }
}
}
Communication Template
To PM: Found root cause - our audio session config allowed Apple Music to take over.
Fix implemented: 3 changes to audio session handling.
Testing: Verified fix with Apple Music, Spotify, phone calls.
ETA: 20 more minutes for full regression test.
To QA: Please test this flow:
1. Play audio in our app
2. Open Apple Music, play a song
3. Return to our app, tap play
4. Lock screen should show OUR controls
Time Saved
- 2-3 hours of debugging speculation
- Launch delay avoided
- QA confidence restored
Scenario 2: Artwork Flickers Every Track Change
Situation
- User feedback: "Album art keeps flashing when songs change"
- Analytics show 3-4 artwork updates per track change
- Designer: "This looks unprofessional"
Root Cause
Multiple artwork sources racing:
- Cache check (async)
- Remote URL fetch (async)
- Embedded artwork extraction (async)
All three complete at different times, each updating Now Playing
Fix (20 minutes)
private var artworkTask: Task<Void, Never>?
func loadArtwork(for track: Track) {
artworkTask?.cancel()
artworkTask = Task { @MainActor in
let artwork = await loadBestArtwork(for: track)
guard !Task.isCancelled else { return }
updateNowPlayingArtwork(artwork, for: track.artworkURL)
}
}
private func loadBestArtwork(for track: Track) async -> UIImage? {
if let embedded = await extractEmbeddedArtwork(track) {
return embedded
}
if let cached = await loadFromCache(track.artworkURL) {
cached
}
downloadImage(track.artworkURL)
}
Communication Template
To Designer: Fixed artwork flicker - reduced from 3-4 updates to 1 per track.
Root cause: Multiple async sources racing to update artwork.
Solution: Task cancellation + priority order (embedded > cached > remote).
Testing: Verified with 10 track changes, zero flicker.
Time Saved
- 1-2 hours investigating image caching
- Designer approval unblocked
- Professional UX restored
Common Gotchas
| Symptom | Cause | Solution | Time to Fix |
|---|
| Info never appears | Missing background mode | Add audio to UIBackgroundModes in Info.plist | 2 min |
| Info never appears | AVAudioSession not activated | Call setActive(true) before playback | 5 min |
| Info never appears | No command handlers | Add target to at least one command | 10 min |
| Info never appears | Using .mixWithOthers | Remove .mixWithOthers option | 5 min |
| Commands grayed out | isEnabled = false | Set command.isEnabled = true after adding target | 5 min |
| Commands don't respond | Handler returns wrong status | Return .success from handler | 5 min |
| Commands don't respond | Using shared command center with MPNowPlayingSession | Use session.remoteCommandCenter instead | 10 min |
| Skip buttons missing | No preferredIntervals | Set skipCommand.preferredIntervals = [15.0] | 5 min |
| Artwork never appears | MPMediaItemArtwork block returns nil | Ensure image is loaded before creating artwork | 15 min |
| Artwork flickers | Multiple rapid updates | Single source of truth with cancellation | 20 min |
| Wrong play/pause state | Using playbackState property | Use playbackRate (1.0 = playing, 0.0 = paused) | 10 min |
| Progress bar stuck | Not updating on seek | Update elapsedPlaybackTime after seek completes | 10 min |
| Progress bar jumps | Updating elapsed on timer | Don't update on timer; system infers from rate | 10 min |
| Loses Now Playing to other apps | Session not reactivated on foreground | Call becomeActiveIfPossible() on foreground |
Expert Checklist
Before Implementing Now Playing
AVAudioSession Setup
Remote Commands
Now Playing Info
Artwork
Testing
CarPlay (if applicable)
Resources
WWDC: 2022-110338, 2017-251, 2019-501
Docs: /mediaplayer/mpnowplayinginfocenter, /mediaplayer/mpremotecommandcenter, /mediaplayer/mpnowplayingsession
Skills: axiom-avfoundation-ref, axiom-now-playing-carplay, axiom-now-playing-musickit
Last Updated: 2026-01-04
Status: iOS 18+ discipline skill covering Now Playing, CarPlay, and MusicKit integration
Tested: Based on WWDC 2019-501, WWDC 2022-110338 patterns