CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.

80

Quality

100%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

musickit-patterns.mdskills/musickit/references/

MusicKit + MediaPlayer Extended Patterns

Overflow reference for the musickit skill. Contains advanced patterns that exceed the main skill file's scope.

Contents

  • MusicKit SwiftUI Integration
  • Genre and Chart Browsing
  • Library Management
  • Playlist Access
  • Now Playing Session
  • Background Audio Configuration

MusicKit SwiftUI Integration

Music Player Manager with @Observable

import MusicKit
import MediaPlayer

@Observable
@MainActor
final class MusicPlayerManager {
    let player = ApplicationMusicPlayer.shared

    var currentSong: Song?
    var isPlaying = false
    var playbackTime: TimeInterval = 0
    var queue: [Song] = []
    var hasSubscription = false

    func setup() async {
        // Check authorization
        let status = await MusicAuthorization.request()
        guard status == .authorized else { return }

        // Check subscription
        if let subscription = try? await MusicSubscription.current {
            hasSubscription = subscription.canPlayCatalogContent
        }

        // Observe subscription changes
        Task {
            for await subscription in MusicSubscription.subscriptionUpdates {
                hasSubscription = subscription.canPlayCatalogContent
            }
        }
    }

    func play(_ song: Song) async throws {
        guard hasSubscription else { return }
        player.queue = [song]
        try await player.play()
        currentSong = song
        isPlaying = true
    }

    func togglePlayPause() {
        if player.state.playbackStatus == .playing {
            player.pause()
            isPlaying = false
        } else {
            Task {
                try? await player.play()
                isPlaying = true
            }
        }
    }

    func skip() async {
        try? await player.skipToNextEntry()
    }
}

SwiftUI Player View

import SwiftUI
import MusicKit

struct MiniPlayerView: View {
    @Environment(MusicPlayerManager.self) private var manager

    var body: some View {
        HStack {
            if let song = manager.currentSong {
                if let artwork = song.artwork {
                    ArtworkImage(artwork, width: 44, height: 44)
                        .clipShape(.rect(cornerRadius: 6))
                }

                VStack(alignment: .leading) {
                    Text(song.title)
                        .font(.subheadline)
                        .lineLimit(1)
                    Text(song.artistName)
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }

                Spacer()

                Button {
                    manager.togglePlayPause()
                } label: {
                    Image(systemName: manager.isPlaying ? "pause.fill" : "play.fill")
                        .font(.title2)
                }

                Button {
                    Task { await manager.skip() }
                } label: {
                    Image(systemName: "forward.fill")
                }
            }
        }
        .padding(.horizontal)
        .frame(height: 60)
    }
}

Search View

struct MusicSearchView: View {
    @State private var searchText = ""
    @State private var results: MusicItemCollection<Song> = []
    @Environment(MusicPlayerManager.self) private var manager

    var body: some View {
        NavigationStack {
            List(results) { song in
                Button {
                    Task { try? await manager.play(song) }
                } label: {
                    HStack {
                        if let artwork = song.artwork {
                            ArtworkImage(artwork, width: 50, height: 50)
                                .clipShape(.rect(cornerRadius: 4))
                        }
                        VStack(alignment: .leading) {
                            Text(song.title)
                            Text(song.artistName)
                                .font(.caption)
                                .foregroundStyle(.secondary)
                        }
                    }
                }
            }
            .searchable(text: $searchText, prompt: "Search Apple Music")
            .onChange(of: searchText) {
                Task { await search() }
            }
            .navigationTitle("Search")
        }
    }

    private func search() async {
        guard !searchText.isEmpty else {
            results = []
            return
        }
        var request = MusicCatalogSearchRequest(term: searchText, types: [Song.self])
        request.limit = 25
        if let response = try? await request.response() {
            results = response.songs
        }
    }
}

Genre and Chart Browsing

Fetching Top Charts

func fetchTopSongs() async throws -> MusicItemCollection<Song> {
    var request = MusicCatalogChartsRequest(
        genre: nil,
        kinds: [.mostPlayed],
        types: [Song.self]
    )
    request.limit = 50
    let response = try await request.response()
    return response.songCharts.first?.items ?? []
}

Fetching by Genre

func fetchSongsByGenre(_ genre: Genre) async throws -> MusicItemCollection<Song> {
    var request = MusicCatalogSearchRequest(term: genre.name, types: [Song.self])
    request.limit = 25
    let response = try await request.response()
    return response.songs
}

Library Management

Library writes require MusicKit authorization and an enabled iCloud Music Library. Check MusicSubscription.current.hasCloudLibraryEnabled before adding items or modifying playlists.

Adding to Library

func addToLibrary(_ song: Song) async throws {
    guard MusicAuthorization.currentStatus == .authorized else { return }
    let subscription = try await MusicSubscription.current
    guard subscription.hasCloudLibraryEnabled else { return }

    try await MusicLibrary.shared.add(song)
}

func addAlbumToLibrary(_ album: Album) async throws {
    guard MusicAuthorization.currentStatus == .authorized else { return }
    let subscription = try await MusicSubscription.current
    guard subscription.hasCloudLibraryEnabled else { return }

    try await MusicLibrary.shared.add(album)
}

Fetching Library Content

func fetchLibrarySongs() async throws -> MusicItemCollection<Song> {
    var request = MusicLibraryRequest<Song>()
    request.sort(by: \.lastPlayedDate, ascending: false)
    request.limit = 50
    let response = try await request.response()
    return response.items
}

Playlist Access

Fetching User Playlists

func fetchPlaylists() async throws -> MusicItemCollection<Playlist> {
    var request = MusicLibraryRequest<Playlist>()
    request.sort(by: \.lastModifiedDate, ascending: false)
    let response = try await request.response()
    return response.items
}

Adding Tracks to a Playlist

func addToPlaylist(_ playlist: Playlist, songs: [Song]) async throws {
    guard MusicAuthorization.currentStatus == .authorized else { return }
    let subscription = try await MusicSubscription.current
    guard subscription.hasCloudLibraryEnabled else { return }

    try await MusicLibrary.shared.add(songs, to: playlist)
}

Now Playing Session

Use MPNowPlayingSession when your app manages multiple simultaneous audio sessions (e.g., picture-in-picture video plus background music).

import MediaPlayer
import AVFoundation

func createNowPlayingSession(for player: AVPlayer) -> MPNowPlayingSession {
    let session = MPNowPlayingSession(players: [player])

    // Session-scoped remote command center
    session.remoteCommandCenter.playCommand.addTarget { _ in
        player.play()
        return .success
    }

    session.remoteCommandCenter.pauseCommand.addTarget { _ in
        player.pause()
        return .success
    }

    // Session-scoped now playing info
    session.nowPlayingInfoCenter.nowPlayingInfo = [
        MPMediaItemPropertyTitle: "Track Title",
        MPMediaItemPropertyArtist: "Artist Name"
    ]

    // Activate this session to become the "now playing" app
    session.becomeActiveIfPossible { success in
        print("Now playing session active: \(success)")
    }

    return session
}

Background Audio Configuration

Audio Session Setup

Configure the audio session before starting playback to enable background audio.

import AVFoundation

func configureAudioSession() throws {
    let session = AVAudioSession.sharedInstance()
    try session.setCategory(.playback, mode: .default)
    try session.setActive(true)
}

Info.plist Background Mode

Add audio to UIBackgroundModes in Info.plist:

<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
</array>

Handling Interruptions

func observeInterruptions() {
    NotificationCenter.default.addObserver(
        forName: AVAudioSession.interruptionNotification,
        object: AVAudioSession.sharedInstance(),
        queue: .main
    ) { notification in
        guard let typeValue = notification.userInfo?[AVAudioSessionInterruptionTypeKey] as? UInt,
              let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
            return
        }

        switch type {
        case .began:
            // Pause UI, save state
            break
        case .ended:
            let options = notification.userInfo?[AVAudioSessionInterruptionOptionKey] as? UInt
            if let options, AVAudioSession.InterruptionOptions(rawValue: options).contains(.shouldResume) {
                // Resume playback
            }
        @unknown default:
            break
        }
    }
}

Route Change Handling

Pause playback when headphones are unplugged to avoid unexpected speaker output.

func observeRouteChanges() {
    NotificationCenter.default.addObserver(
        forName: AVAudioSession.routeChangeNotification,
        object: AVAudioSession.sharedInstance(),
        queue: .main
    ) { notification in
        guard let reasonValue = notification.userInfo?[AVAudioSessionRouteChangeReasonKey] as? UInt,
              let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
            return
        }

        if reason == .oldDeviceUnavailable {
            // Headphones were unplugged -- pause playback
            pausePlayback()
        }
    }
}

skills

.mcp.json

README.md

tile.json