๐ŸŽต MusicKit

Apple Music์„ ์•ฑ์— ํ†ตํ•ฉํ•˜๋Š” ๊ฐ•๋ ฅํ•œ ํ”„๋ ˆ์ž„์›Œํฌ

iOS 9.3+Apple Music ํ†ตํ•ฉ

โœจ MusicKit์ด๋ž€?

MusicKit์€ Apple Music์˜ ๋ฐฉ๋Œ€ํ•œ ์นดํƒˆ๋กœ๊ทธ์™€ ์‚ฌ์šฉ์ž์˜ ๊ฐœ์ธ ์Œ์•… ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค. ์Œ์•… ์žฌ์ƒ, ๊ฒ€์ƒ‰, ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ ์ƒ์„ฑ, ์ถ”์ฒœ ์Œ์•… ํ‘œ์‹œ ๋“ฑ์„ ์•ฑ์—์„œ ์ง์ ‘ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, Apple Music ๊ตฌ๋…์ž์—๊ฒŒ ์™„์ „ํ•œ ์ŠคํŠธ๋ฆฌ๋ฐ ๊ฒฝํ—˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ’ก ํ•ต์‹ฌ ๊ธฐ๋Šฅ: Apple Music ์นดํƒˆ๋กœ๊ทธ ๊ฒ€์ƒ‰ ยท ์Œ์•… ์žฌ์ƒ ยท ์‚ฌ์šฉ์ž ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ ‘๊ทผ ยท ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ ๊ด€๋ฆฌ ยท ์ตœ๊ทผ ์žฌ์ƒ ๋ชฉ๋ก ยท ์ถ”์ฒœ ์Œ์•… ยท ๊ตฌ๋… ์ƒํƒœ ํ™•์ธ

๐Ÿ”‘ 1. ๊ถŒํ•œ ์š”์ฒญ ๋ฐ ์ดˆ๊ธฐํ™”

MusicKit ์‚ฌ์šฉ ์ „ ์‚ฌ์šฉ์ž์˜ Apple Music ๊ถŒํ•œ์„ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค.

MusicAuthManager.swift โ€” ๊ถŒํ•œ ์š”์ฒญ
import MusicKit
import SwiftUI

@Observable
class MusicAuthManager {
    var authorizationStatus: MusicAuthorization.Status = .notDetermined
    var isSubscriptionActive = false

    // ๊ถŒํ•œ ์š”์ฒญ
    func requestAuthorization() async {
        let status = await MusicAuthorization.request()
        authorizationStatus = status

        switch status {
        case .authorized:
            print("โœ… Apple Music ๊ถŒํ•œ ์Šน์ธ๋จ")
            await checkSubscriptionStatus()
        case .denied:
            print("โŒ Apple Music ๊ถŒํ•œ ๊ฑฐ๋ถ€๋จ")
        case .restricted:
            print("โš ๏ธ Apple Music ๊ถŒํ•œ ์ œํ•œ๋จ")
        case .notDetermined:
            print("๐Ÿค” Apple Music ๊ถŒํ•œ ๋ฏธ๊ฒฐ์ •")
        @unknown default:
            break
        }
    }

    // ๊ตฌ๋… ์ƒํƒœ ํ™•์ธ
    func checkSubscriptionStatus() async {
        let subscription = MusicSubscription.current
        isSubscriptionActive = subscription?.canPlayCatalogContent ?? false

        if isSubscriptionActive {
            print("๐ŸŽต Apple Music ๊ตฌ๋… ํ™œ์„ฑ")
        } else {
            print("๐Ÿ“ฑ Apple Music ๊ตฌ๋… ํ•„์š”")
        }
    }

    // ํ˜„์žฌ ๊ถŒํ•œ ์ƒํƒœ ํ™•์ธ
    func checkCurrentStatus() {
        authorizationStatus = MusicAuthorization.currentStatus
    }
}

๐ŸŽง 2. ์Œ์•… ์žฌ์ƒ

ApplicationMusicPlayer๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์•ฑ ๋‚ด์—์„œ ์Œ์•…์„ ์žฌ์ƒํ•ฉ๋‹ˆ๋‹ค.

MusicPlayer.swift โ€” ์Œ์•… ์žฌ์ƒ
import MusicKit

@Observable
class MusicPlayerManager {
    let player = ApplicationMusicPlayer.shared
    var currentSong: Song?
    var isPlaying = false
    var playbackTime: TimeInterval = 0

    // ๋‹จ์ผ ๊ณก ์žฌ์ƒ
    func play(song: Song) async {
        do {
            player.queue = [song]
            try await player.play()
            currentSong = song
            isPlaying = true
            print("โ–ถ๏ธ ์žฌ์ƒ: \(song.title)")
        } catch {
            print("โŒ ์žฌ์ƒ ์‹คํŒจ: \(error)")
        }
    }

    // ์•จ๋ฒ” ์žฌ์ƒ
    func play(album: Album) async {
        do {
            // ์•จ๋ฒ”์˜ ๋ชจ๋“  ํŠธ๋ž™ ๊ฐ€์ ธ์˜ค๊ธฐ
            let detailedAlbum = try await album.with([.tracks])
            guard let tracks = detailedAlbum.tracks else { return }

            player.queue = ApplicationMusicPlayer.Queue(tracks)
            try await player.play()
            isPlaying = true
        } catch {
            print("โŒ ์•จ๋ฒ” ์žฌ์ƒ ์‹คํŒจ: \(error)")
        }
    }

    // ์žฌ์ƒ/์ผ์‹œ์ •์ง€ ํ† ๊ธ€
    func togglePlayPause() async {
        if player.state.playbackStatus == .playing {
            player.pause()
            isPlaying = false
        } else {
            do {
                try await player.play()
                isPlaying = true
            } catch {
                print("โŒ ์žฌ์ƒ ์‹คํŒจ: \(error)")
            }
        }
    }

    // ๋‹ค์Œ ๊ณก
    func skipToNext() async {
        do {
            try await player.skipToNextEntry()
        } catch {
            print("โŒ ๋‹ค์Œ ๊ณก ์ด๋™ ์‹คํŒจ")
        }
    }

    // ์ด์ „ ๊ณก
    func skipToPrevious() async {
        do {
            try await player.skipToPreviousEntry()
        } catch {
            print("โŒ ์ด์ „ ๊ณก ์ด๋™ ์‹คํŒจ")
        }
    }

    // ์žฌ์ƒ ์œ„์น˜ ๋ณ€๊ฒฝ
    func seek(to time: TimeInterval) {
        player.playbackTime = time
    }

    // ์…”ํ”Œ ๋ชจ๋“œ ํ† ๊ธ€
    func toggleShuffle() {
        player.state.shuffleMode = player.state.shuffleMode == .off ? .songs : .off
    }

    // ๋ฐ˜๋ณต ๋ชจ๋“œ ๋ณ€๊ฒฝ
    func cycleRepeatMode() {
        switch player.state.repeatMode {
        case .none:
            player.state.repeatMode = .all
        case .all:
            player.state.repeatMode = .one
        case .one:
            player.state.repeatMode = .none
        @unknown default:
            player.state.repeatMode = .none
        }
    }
}

๐Ÿ” 3. ์นดํƒˆ๋กœ๊ทธ ๊ฒ€์ƒ‰

Apple Music ์นดํƒˆ๋กœ๊ทธ์—์„œ ์Œ์•…์„ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.

MusicSearch.swift โ€” ์นดํƒˆ๋กœ๊ทธ ๊ฒ€์ƒ‰
import MusicKit

@Observable
class MusicSearchManager {
    var searchResults: MusicCatalogSearchResponse?
    var songs: [Song] = []
    var albums: [Album] = []
    var artists: [Artist] = []
    var playlists: [Playlist] = []

    // ํ†ตํ•ฉ ๊ฒ€์ƒ‰
    func search(term: String) async {
        do {
            var request = MusicCatalogSearchRequest(
                term: term,
                types: [Song.self, Album.self, Artist.self, Playlist.self]
            )
            request.limit = 25

            let response = try await request.response()
            searchResults = response

            songs = Array(response.songs)
            albums = Array(response.albums)
            artists = Array(response.artists)
            playlists = Array(response.playlists)

            print("๐Ÿ” ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ: ๋…ธ๋ž˜ \(songs.count), ์•จ๋ฒ” \(albums.count)")
        } catch {
            print("โŒ ๊ฒ€์ƒ‰ ์‹คํŒจ: \(error)")
        }
    }

    // ๋…ธ๋ž˜๋งŒ ๊ฒ€์ƒ‰
    func searchSongs(term: String) async {
        do {
            var request = MusicCatalogSearchRequest(term: term, types: [Song.self])
            request.limit = 50

            let response = try await request.response()
            songs = Array(response.songs)
        } catch {
            print("โŒ ๋…ธ๋ž˜ ๊ฒ€์ƒ‰ ์‹คํŒจ: \(error)")
        }
    }

    // ์•„ํ‹ฐ์ŠคํŠธ ๊ฒ€์ƒ‰
    func searchArtists(term: String) async {
        do {
            var request = MusicCatalogSearchRequest(term: term, types: [Artist.self])
            request.limit = 25

            let response = try await request.response()
            artists = Array(response.artists)
        } catch {
            print("โŒ ์•„ํ‹ฐ์ŠคํŠธ ๊ฒ€์ƒ‰ ์‹คํŒจ: \(error)")
        }
    }

    // ํŠน์ • ID๋กœ ๋…ธ๋ž˜ ๊ฐ€์ ธ์˜ค๊ธฐ
    func fetchSong(id: MusicItemID) async -> Song? {
        do {
            let request = MusicCatalogResourceRequest<Song>(matching: \.id, equalTo: id)
            let response = try await request.response()
            return response.items.first
        } catch {
            print("โŒ ๋…ธ๋ž˜ ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ: \(error)")
            return nil
        }
    }
}

๐Ÿ“š 4. ์‚ฌ์šฉ์ž ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

์‚ฌ์šฉ์ž์˜ Apple Music ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์ ‘๊ทผํ•ฉ๋‹ˆ๋‹ค.

MusicLibrary.swift โ€” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ ‘๊ทผ
import MusicKit

@Observable
class MusicLibraryManager {
    var librarySongs: [Song] = []
    var libraryPlaylists: [Playlist] = []
    var recentlyPlayed: [RecentlyPlayedMusicItem] = []

    // ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ๋ชจ๋“  ๋…ธ๋ž˜ ๊ฐ€์ ธ์˜ค๊ธฐ
    func fetchLibrarySongs() async {
        do {
            let request = MusicLibraryRequest<Song>()
            let response = try await request.response()
            librarySongs = Array(response.items)
            print("๐Ÿ“š ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋…ธ๋ž˜: \(librarySongs.count)๊ฐœ")
        } catch {
            print("โŒ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋…ธ๋ž˜ ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ: \(error)")
        }
    }

    // ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ
    func fetchLibraryPlaylists() async {
        do {
            let request = MusicLibraryRequest<Playlist>()
            let response = try await request.response()
            libraryPlaylists = Array(response.items)
            print("๐Ÿ“š ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ: \(libraryPlaylists.count)๊ฐœ")
        } catch {
            print("โŒ ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ: \(error)")
        }
    }

    // ์ตœ๊ทผ ์žฌ์ƒ ๋ชฉ๋ก
    func fetchRecentlyPlayed() async {
        do {
            let request = MusicRecentlyPlayedRequest<RecentlyPlayedMusicItem>()
            let response = try await request.response()
            recentlyPlayed = Array(response.items)
            print("๐Ÿ• ์ตœ๊ทผ ์žฌ์ƒ: \(recentlyPlayed.count)๊ฐœ")
        } catch {
            print("โŒ ์ตœ๊ทผ ์žฌ์ƒ ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ: \(error)")
        }
    }

    // ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ๋…ธ๋ž˜ ์ถ”๊ฐ€
    func addToLibrary(song: Song) async {
        do {
            try await MusicLibrary.shared.add(song)
            print("โž• ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์ถ”๊ฐ€: \(song.title)")
        } catch {
            print("โŒ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ถ”๊ฐ€ ์‹คํŒจ: \(error)")
        }
    }

    // ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์•จ๋ฒ” ์ถ”๊ฐ€
    func addToLibrary(album: Album) async {
        do {
            try await MusicLibrary.shared.add(album)
            print("โž• ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์•จ๋ฒ” ์ถ”๊ฐ€: \(album.title)")
        } catch {
            print("โŒ ์•จ๋ฒ” ์ถ”๊ฐ€ ์‹คํŒจ: \(error)")
        }
    }
}

๐ŸŽผ 5. ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ ๊ด€๋ฆฌ

ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

PlaylistManager.swift โ€” ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ
import MusicKit

@Observable
class PlaylistManager {
    var playlists: [Playlist] = []

    // ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ ์ƒ์„ฑ
    func createPlaylist(name: String, description: String, songs: [Song]) async {
        do {
            let playlist = try await MusicLibrary.shared.createPlaylist(
                name: name,
                description: description,
                items: songs
            )
            print("โœ… ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ ์ƒ์„ฑ: \(playlist.name)")
        } catch {
            print("โŒ ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ ์ƒ์„ฑ ์‹คํŒจ: \(error)")
        }
    }

    // ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ์— ๊ณก ์ถ”๊ฐ€
    func addSongs(to playlist: Playlist, songs: [Song]) async {
        do {
            try await MusicLibrary.shared.add(songs, to: playlist)
            print("โž• \(songs.count)๊ณก ์ถ”๊ฐ€๋จ")
        } catch {
            print("โŒ ๊ณก ์ถ”๊ฐ€ ์‹คํŒจ: \(error)")
        }
    }

    // ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ ์ƒ์„ธ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
    func fetchPlaylistDetails(playlist: Playlist) async -> Playlist? {
        do {
            let detailed = try await playlist.with([.tracks, .curator])
            return detailed
        } catch {
            print("โŒ ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ ์ƒ์„ธ ์ •๋ณด ์‹คํŒจ: \(error)")
            return nil
        }
    }
}

๐Ÿ’ก HIG ๊ฐ€์ด๋“œ๋ผ์ธ

๐ŸŽฏ ์‹ค์ „ ํ™œ์šฉ

๐Ÿ“š ๋” ์•Œ์•„๋ณด๊ธฐ

โšก๏ธ ์„ฑ๋Šฅ ํŒ: MusicCatalogSearchRequest์˜ limit์„ ์ ์ ˆํžˆ ์„ค์ •ํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ๋ฅผ ๋ฐฉ์ง€ํ•˜์„ธ์š”. ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ๊ตฌํ˜„ํ•˜๋ฉด ๋” ๋‚˜์€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.