[WWDC-2021] Meet MusicKit for Swift

2021. 8. 11. 22:00iOS/WWDC

이 글은 WWDC-2021 영상을 정리하여 작성한 글입니다.

원본 영상은 링크를 참고하여 주세요.

Meet MusicKit for Swift

오늘 소개해드릴 영상의 주제는 MusicKit 입니다. 이름만 보아도 어떤 API 인지 느낌이 오시지 않나요 ?!

MusicKit은 말그대로 스위프트를 통해 음악항목에 엑세스할 수 있는 새로운 프레임워크입니다.

MusicKit은 Swift의 새로운 동시성 구문을 활용하고, 처음부터 SwiftUI와 함께 사용하도록 설계되었습니다. 또한 Apple Music의 API와 앱의 통합 방식을 가속화하여 Apple Music에 연결되는 앱을 쉽게 제작할 수 있습니다.

위 세션은 세가지 스텝으로 진행됩니다. 먼저, MusicKit으로 음악 콘텐츠를 요청하는 방법에 대해 알아보고 앱이 Apple Music의 데이터에 엑세스하는데 필요한 사용자의 동의 요청과 Apple Music API에 엑세스하는 데 필요한 토큰 관리, 구독 정보 및 관련 기능 엑세스, Apple Music 카탈로그에서 음악을 재성하는 방법에 대해 알아보게 됩니다. 마지막으로 사용자가 아직 Apple Music에 가입하지 않았다면, 구독을 추천하는 방법을 알게 됩니다.

Music requests

MusicKit은 Apple Music API에서 콘텐츠를 가져올 수 있는 구조화된 요청을 통해 음악 항목에 엑세스할 수 있는 새로운 모델 계층을 제공합니다. 이러한 새로운 모델 계층을 통해 Apple Music 카탈로그의 컨텐츠를 검색하거나, 특정 필터에 따라 리소스들을 가져올 수 있게 됩니다. 이러한 요청들은 Collection으로 그룹화된 응답이며, pagination과 같은 기능도 제공합니다.

Album의 예시를 보며 살펴봅시다.

Album은 세가지 카테고리들로 그룹화된 값 타입의 구조 입니다. 속성들을 살펴보면 title, isCompilation, artwork가 있는데, artwork의 경우에는 크기정보 및 관련 색상과 아트워크 이미지 URL에 엑세스할 수 있는 구조를 가지고 있습니다.

 앨범은 또한 아티스트, 장르, 앨범의 트랙목록과 같은 relationship 을 제공합니다. 예를 들어 트랙의 경우, Track이라는 타입의 Collection 형태로 제공됩니다.

 마지막으로 Album과 직접적인 관계는 아니지만 연관된 데이터를 불러오는 속성들로, 예를들어 'appearsOn'의 경우 PlayList의 Collection 현태를 반환하지만, 'tracks' 와는 달리 제목도 같이 전달 됩니다.

이러한 앨범에서 'relationships' 데이터를 가져오는 방법은 매우 간단합니다.

// Loading and accessing relationships

let detailedAlbum = try await album.with([.artists, .tracks, .relatedAlbums])
print("\(detailedAlbum)")

if let tracks = detailedAlbum.tracks {
    print("  Tracks:")
    tracks.prefix(2).forEach { track in
        print("    \(track)")
    }
}

 

먼저 '.artists, .track' 과 같은 속성들을 통해 필요한 정보를 filter할수 있으며, with 메소드는 Swift 5.5의 동시성 코드를 통해 비동기 적인 데이터를 받아올 수 있습니다. 이렇게 받온 앨범 데이터에서 tracks데이터를 일반 배열과 같이 사용할 수 있습니다. 위 코드의 결과는 아래와 같습니다.

이와 비슷하게 relatedAlbums 속성 또한 다음과 같이 배열로써 받아올 수 있습니다. 다른 점은 relatedAlbums내부에는 title이 포함되어 있는 점 입니다.

// Loading and accessing associations

let detailedAlbum = try await album.with([.artists, .tracks, .relatedAlbums])
print("\(detailedAlbum)")

if let relatedAlbums = detailedAlbum.relatedAlbums {
    print("  \(relatedAlbums.title ?? ""):")
    relatedAlbums.prefix(2).forEach { relatedAlbum in
        print("    \(relatedAlbum)")
    }
}

해당 세션에서는 실제 CD에 있는 바코드를 스캔하여 MusicKit을 통해 Apple Music을 통해 음악을 찾는 예제입니다. 예제 코드는 링크를 통해 다운받으실 수 있으며 SwiftUI 로 작성된 코드입니다.

예제코드를 살펴봅시다.

바코드 스캔하는 부분을 살펴보기 전에 저는 한번 간단하게 검색어를 통해 search하는 부분을 먼저 살펴보았습니다. 

    /// Searches for an album matching a scanned barcode.
    private func handleDetectedBarcode(_ detectedBarcode: String) {
        if detectedBarcode.isEmpty {
            self.detectedAlbum = nil
        } else {
            detach {
                do {
                    // DEMO: Request albums matching detectedBarcode.
                    
                    let albumsRequest = MusicCatalogResourceRequest<Album>(matching: \.upc, equalTo: detectedBarcode)
                    let albumsResponse = try await albumsRequest.response()
                    if let firstAlbum = albumsResponse.items.first {
                        await self.handleDetectedAlbum(firstAlbum)
                    }
                } catch {
                    print("Encountered error while trying to find albums with upc = \"\(detectedBarcode)\".")
                }
            }
        }
    }

먼저 카메라 스캔을 통해 받은 'detectedBarcode' 를 받아와서 'MusicCatalogResourceRequest' 라는 구조의 Request를 생성합니다. Request 생성자에서 matching: 은 request를 요청하는 타입이라 볼수 있고, 여기서 '.upc' 는 Universal Product Code의 약자를 뜻합니다. 즉, 이 코드를 통해 MusicKit의 데이터를 search할 수 있습니다.

이러한 request는 .reponse()를 호출하여 비동기 데이터를 받아올 수 있습니다.

그리고 간단한 검색어를 통해 search 할 수 있는 코드도 살펴보았습니다. 이부분은 세션에서는 따로 다루진 않았지만, 한번 확인해보면 좋을것 같습니다.

    /// Makes a new search request to MusicKit when the current search term changes.
    private func requestUpdatedSearchResults(for searchTerm: String) {
        detach {
            if searchTerm.isEmpty {
                await self.reset()
            } else {
                do {
                    // Issue a catalog search request for albums matching search term.
                    var searchRequest = MusicCatalogSearchRequest(term: searchTerm, types: [Album.self])
                    searchRequest.limit = 5
                    let searchResponse = try await searchRequest.response()
                    
                    // Update the user interface with search response.
                    await self.apply(searchResponse, for: searchTerm)
                } catch {
                    print("Search request failed with error: \(error).")
                    await self.reset()
                }
            }
        }
    }

이 부분에서 또한 'MusicCatalogSearchRequest'를 생성하는데요? 다른 부분은 생성자 부분으로 검색어를 넣어주고, 어떤 타입의 데이터인지 지정해 준 다음, limit을 설정하고 .response()를 호출해주는 모습을 볼 수 있습니다.

 

그리고 MusicKit은 URL을 사용하여 Apple Music API의 endpoint에서 콘텐츠 로드가 가능함으로써, 위에서 살펴봤던 구조화된 요청과도 다른 범용적인 데이터 요청 방법을 제공합니다. 

아래 예제를 살펴봅시다.

// Loading top level genres

struct MyGenresResponse: Decodable {
    let data: [Genre]
}

let countryCode = try await MusicDataRequest.currentCountryCode
let url = URL(string: "https://api.music.apple.com/v1/catalog/\(countryCode)/genres")!

let dataRequest = MusicDataRequest(urlRequest: URLRequest(url: url))
let dataResponse = try await dataRequest.response()

let decoder = JSONDecoder()
let genresResponse = try decoder.decode(MyGenresResponse.self, from: dataResponse.data)
print("\(genresResponse.data[9])")

 

만약 어떤 지정된 url이 존재하고 이에대한 api 호출이 필요하다면, 'MusicDataRequest' 내부에 url을 담아 데이터를 요청할 수 있습니다. 이때 데이터는 json 형태로 받아오게 되며, JSONDecoder()를 통해 파싱이 가능해집니다.

Privacy and user consent

지금까지 MusicKit을 활용하여 실제 음악 데이터를 가져오는 작업에 대해 살펴보았다면, 이제부터는 MusicKit을 사용하는 앱에서 사용하기 위한 몇가지 단계(ex 유저 데이터 가져오기)를 살펴보도록 하겠습니다.

실제로 MusicKit을 사용한 앱에서는 실제 사용자가 앱이 사용자 데이터에 엑세스할 수 있는 제어권을 유지하여야 합니다. 따라서, 사용자가 들었던 음악에 대한 기록이나, 음악 보관함에 대한 데이터를 요청하기전에 사용자의 사전 동의를 받아야 합니다. 이러한 작업은 기기마다, 어플리케이션 마다 개별적으로 수행되어야 합니다.

이러한 동의 요청은 데이터에 대한 접근이 처음 필요할 때, 다이얼로그를 통해 권한을 받습니다. 다이얼로그에는 앱이 Apple Music에 엑세스 해야하는 이유를 사용자에게 전달해야하며, 이러한 세부 정보들은 info.plist에 정의하면 다이얼로그의 부제로 포함됩니다.

실제 예제코드를 봅시다.

 

 

// Requesting user consent for MusicKit

@State var isAuthorizedForMusicKit = false

func requestMusicAuthorization() {
    detach {
        let authorizationStatus = await MusicAuthorization.request()
        if authorizationStatus == .authorized {
            isAuthorizedForMusicKit = true
        } else {
            // User denied permission.
        }
    }
}

실제 앱에 MusicKit이 필요한 기능이 있고, 이러 'isAuthorizedForMusicKit' 라는 @State 타입의 변수를 사용하여 이 기능에 엑세스할 수 있다고 생각해 봅시다. (@State 같은 경우 SwiftUI 의 문법 입니다.) MusicKit을 사용하기 전에 먼저, async/await을 사용하여 Apple Music에 엑세스할 수 있는 권한을 요청할 수 있습니다. 이러한 경우 앱이 아직 권한을 획득하지 못했을 때만, 메시지가 표시됩니다.

'MusicAuthorization.request()' 을 통해 상태값을 반환받아와서,상태가 .authorized 인 경우, 아닌경우에 따라 @State 를 변경시켜주거나, 다른 로직을 작성하게되면 끝입니다.

Token management

Apple Music API를 사용하기 위해서는 개발자 토큰이 필요하고, 이 토큰을 사용하여 앱을 인증할 수 있습니다. 이전에는 이 토큰을 얻기 위해 Apple Developer 포털을 통해 MusicKit에 대한 개인 키를 만드는 등의 여러 노력이 필요하였지만, 이제부터는 MusicKit을 사용하면 앱에 대한 개발자 토큰이 자동으로 생성되게 됩니다. 이러한 작업은 Apple Developer 포털에 등록하고, 자동 동작하도록 선택하기만 하면 됩니다.

특히, 위의 앱ID를 등록하는 페이지에서, App Services 탭을 선택하고 MusicKit을 활성화 해주면 끝입니다.

그리고 Apple Music API에는 사용자 지정된 Endpoint에 대해 사용자 토큰이 필요합니다. 그리고 이 사용자 토큰은 개발자 토큰과 마찬가지로 자동으로 생성됩니다.

Subscription Information

이러한 앱을 사용하기 위해선 Apple Music 가입이 활성 상태인지 확인하는 방법 또한 필요합니다. 이러한 정보는 MusicKit을 활용하여

  1. Apple Music 카탈로그의 콘텐츠를 재생할 수 있는지
  2. iCloud Music Library를 사용할 수 있는지
  3. 활성 구독이 없는 경우 구독자가 될 수 있는지

여부를 알 수 있습니다. 이를 활용하기 위해선, 앱에서 Apple Music과 관련된 기능에 대한 체크가 필요합니다.

예제코드를 확인해봅시다.

예를들어, 실제로 음악을 재생하는 기능이 있는 경우, 사용자가 Apple Music의 카탈로그 컨텐츠를 재생할 수 있는지 여부를 체크하여 재생버튼을 비활성화 상태로 유지할 수 있습니다. 

// Using music subscription to drive state of a play button

@State var musicSubscription: MusicSubscription?

var body: some View {
    Button(action: handlePlayButtonSelected) {
        Image(systemName: "play.fill")
    }
    .disabled(!(musicSubscription?.canPlayCatalogContent ?? false))
    .task {
        for await subscription in MusicSubscription.subscriptionUpdates {
            musicSubscription = subscription
        }
    }
    /// .task { } 는 async/await을 활용한 SwiftUI 문법입니다. 프로젝트 내의 주석을 첨부합니다.
    /// Adds a task to perform when this view appears or a specified
    /// value changes
    ///
    /// The running task will be cancelled either when the value
    /// changes causing a new task to start or when this view
    /// disappears.
}

'MusicSubscription' 타입의  @State 변수를 설정해 줌으로써 '.canPlayCatalogContent' 속성을 통해 비활성화 여부를 지정해주고, '.task'로 지정된 비동기 블록 내에서 구독 정보에 대해 새롭게 업데이트 되는 정보를 사용할 수 있습니다.

Playback

이제 MusicKit으로 실제 음악을 재생하는 방법에 대해 알아보겠습니다.

MusicKit은 'SystemMusicPlayer' 와 'ApplicationMusicPlayer'라고 부르는 두가지 플레이어를 제공합니다. 이 두 플레이어의 차이점을 예로 들어봅시다.

'SystemMusicPlayer'를 사용하는 소셜 미디어 앱이 있을 때, 이 앱은 플레이어를 사용하여 시스템 음악 앱에서 재생되는 내용을 변경할 수 있지만, 'ApplicationMusicPlayer'를 사용하는 피트니스 앱이 있는 경우, 이 앱에서는 재생상태를 시스템 음악 앱과 완전히 독립적으로 유지할 수 있습니다.

두 플레이어는 현재 재생중인 정보를 자동으로 보고하고, 원격 명령을 처리하며, 이것은 잠금 화면의 시스템 미디어 컨트롤과 긴밀하게 통합되는 기능을 제공합니다. 하지만, SystemMusicPlayer를 사용한다면, 음악 앱이 현재 재생중인 앱으로 표시되며, ApplicationMusicPlayer를 사용하면 현재 재생중인 앱으로 표시됩니다.

또한  SystemMusicPlayer는 재생대기열에 있어서 기본 음악 앱을 원격 제어하는데 그치는 반면, ApplicationMusicPlayer에서는 앱이 별도의 재생 대기열을 소유합니다. 따라서 두 플레이어는 하나이상의 항목으로 재생목록을 설정하고, 다음 재생할 노래를 추가하여 재생할 순 있지만, Application Music Player만이 재생 대기열을 추가로 제어하여 중간에 항목을 삽입하거나, 이전에 추가된 항목을 제거할 수 있습니다.

Subscription Offers

마지막으로 사용자가 Apple Music 가입자가 아닌 경우 앱내에서 Apple Music 무료 평가판을 시작할 수 있도록 허용할 수 있습니다. 이러한 Offer를 제시하는 상황은 특정한 노래, 앨범 또는 플레이리스트로 제한이 가능합니다.

특히나, 앱에서 구독을 제안하는 Sheet를 사용하는 경우 'Apple Services Performance Partners Program' 이라는 제휴 프로그램을 통해 새로운 가입자룰 유치한 것에 대해 보상을 받을 수 있습니다.(??????!)

이러한 Offer를 표시하기 위해 앞서 살펴본 Music 구독 내역을 추적해야 합니다. 예제를 봅시다.

// Showing contextual music subscription offer

@State var musicSubscription: MusicSubscription?
@State var isShowingOffer = false

var offerOptions: MusicSubscriptionOffer.Options {
    var offerOptions = MusicSubscriptionOffer.Options()
    offerOptions.itemID = album.id
    return offerOptions
}

var body: some View {
    Button("Show Subscription Offers", action: showSubscriptionOffer)
        .disabled(!(musicSubscription?.canBecomeSubscriber ?? false))
        .musicSubscriptionOffer(isPresented: $isShowingOffer, options: offerOptions)
}

func showSubscriptionOffer() {
    isShowingOffer = true
}

위의 구독내역을 체크하는 부분에 있어서 추가적으로 이제 구독을 제안하는 화면이 표시되었는지 여부를 추적하기 위해 'isShowingOffer' 라는 @State변수를 추가해주었습니다. 여기서는 offer 뷰의 옵션으로 앨범 id를 지정해 주었습니다.

그리고 'canBecomeSubscriber' 속성에 따라 "Show Subscription Offers" 버튼의 비활성화 여부를 설정해 줍니다. 그 다음 isShowingOffer 변수에 대한 바인딩과 함께, '.musicSubscriptionOffer' 라는 수정자를 사용하여 간단하게 offer 뷰를 띄워줄 수 있습니다.

여기까지 MusicKit의 다양한 활용 방법에 대해 알아보았습니다. 모든 것을 세세하게 다루진 않았지만, MusicKit 자체가 굉장히 흥미롭고, 한번 써보고 싶다는 생각이 많이 들었네요. !!

번역에 가까운 글이였지만, 틀린 부분이 있다거나 요상한(?) 부분이 있다면 댓글남겨주세요.

감사합니다.