CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

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

71

Quality

89%

Does it follow best practices?

Impact

No eval scenarios have been run

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(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

Adding to Library

func addToLibrary(_ song: Song) async throws {
    try await MusicLibrary.shared.add(song)
}

func addAlbumToLibrary(_ album: Album) async throws {
    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 {
    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

CHANGELOG.md

README.md

tile.json