Integrating Apple Music Into Your iOS App

We recently released a major update to Streaks Workout, with one of the new features automatic playback from Apple Music.

While the actual playback of songs from Apple Music is somewhat straightforward, the overall integration process is a little more complicated as it uses private keys, token generation, and a JSON API.

MusicKit logo.

There are two key parts to using MusicKit:

  1. Finding content using the Apple Music API.
  2. Playing content using the MediaPlayer framework.

Note: At time of writing, you can access the Apple Music API from tvOS and watchOS, but you can only stream music from iOS.

There is a lot of information published by Apple about this topic, but I found it confusing at first trying to piece it all together.

Our Implementation

In Streaks Workout, the app uses Apple Music as follows:

  1. User enables the Apple Music option.
  2. The app finds workouts-related playlists.
  3. The user can choose which playlist is active.
  4. During a workout, the app plays the selected workout in shuffle mode.
Playlist selection screen in Streaks Workout

Apple Music API

The Apple Music API is a web service that lets you search or lookup songs, artists, albums and playlists. You can also find information related to the current user, such as their recommended playlists.

As it's a JSON-based API, there is no framework built into iOS to access it: you must roll your own (or use a third-party implementation) using URL loading classes, such as URLSession.

Before that though, you need a developer token in order to access the API.

Generating a Developer Token

The developer token is a JWT (JSON web token). Information for creating one is available from https://developer.apple.com/documentation/applemusicapi/getting_keys_and_creating_tokens.

In short, you need to:

  1. Create a music identifier for your app in the Apple Developer Portal.
  2. Create a private key with MusicKey enabled and associate it with the music ID.
  3. Take note of the music ID, the team ID for your app, and download the private key.

Next, you can create a developer token using these three pieces of information.

I used the ruby-jwt package to generate the developer token, as it was able to run on macOS without needing to install a bunch of dependencies.

Note: A developer token has a finite lifetime. The longest it can be valid for is 6 months. As such, it must be regenerated - either manually or automatically - periodically.

Once you supply the IDs and key to ruby-jwt, it will output a long string of characters. This is your developer token. You will use this directly in your iOS app.

A fake token I typed out that looks like a real one. I'd be amazed if it actually worked.

Determining The User's Storefront

When querying the API, catalog requests are tied to a specific storefront (or country). You can determine this country code using the SKCloudServiceController class.

import StoreKit

let controller = SKCloudServiceController()

controller.requestStorefrontCountryCode { countryCode, error in
    // Use the value in countryCode for subsequent API requests
}

The only time the country isn't needed is when requesting information related to the current user, such as recommendations. In this case, you'd need a user token instead.

Determining the Current User's Token

Some API calls require a user token. This corresponds to the user of the current device.

import StoreKit

let developerToken = "..."
let controller = SKCloudServiceController()

controller.requestUserToken(forDeveloperToken: developerToken) { userToken, error in
    // Use this value for recommendation requests.
}

You may want to cache the user token for some period of time so you don't have to request it every single time (it could be slow).

Determining the API URL

There are a number of different API endpoints, depending on what you're trying to request.

For example, the following URL would be used to search for playlists. Note the use of countryCode that we determined earlier:


let searchTerm  = "workouts"
let countryCode = "us"

var components = URLComponents()
components.scheme = "https"
components.host   = "api.music.apple.com"
components.path   = "/v1/catalog/\(countryCode)/search"
        
components.queryItems = [
    URLQueryItem(name: "term", value: searchTerm),
    URLQueryItem(name: "limit", value: "25"),
    URLQueryItem(name: "types", value: "playlists"),
]

let url = urlComponents.url

In our implementation, we constructed the URL using URLComponents, since this is much more flexible and easier to modify as required.

More information about the API endpoints can be found at https://developer.apple.com/documentation/applemusicapi.

Performing an API Request

To perform a catalog search, you need the developer token and the storefront country code.

The developer token is included in the HTTP request by adding the Authorization HTTP header.

let url = urlComponents.url // Constructed above

var request = URLRequest(url: url)
request.setValue("Bearer \(developerToken)", forHTTPHeaderField: "Authorization")

If the user token is needed, it can be added in the Music-User-Token header.

request.setValue(userToken, forHTTPHeaderField: "Music-User-Token")

Note that in the example of performing a catalog search, the user token is not required.

Performing The Request

Once you've constructed the URL and build the URLRequest object, you can perform the request.

let session = URLSession.shared
        
let task = session.dataTask(with: request) { data, response, error in

}

As usual, you should perform error handling, but assuming the response is valid and data contains, JSON, you can parse it by converting it to JSON then extracting the results.

guard let data = data else {
    return 
}

do {
    let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
}
catch {
}

I'm not going to cover extracting the specific data from this JSON object, but if you ouput it to your console you can see how the response is constructed, and the following URL contains much more info about all of the response objects:

You might have luck finding some pre-baked code to access the API, but for my own purposes I just needed search functionality, so crafted it exactly as I required.

Music Playback

After performing your calls on the Apple Music API, you might have determined the unique ID for the item you want to play (that ID might correspond to a song, an album, a playlist or an artist).

In order to play that item, there are several steps to take:

  1. Request access from the user to access their music library.
  2. Check that the user has an active Apple Music account.
  3. Optionally show them a "sign up to Apple Music" screen.
  4. Play the item.

Request Music Library Access

To request authorization, you can do so as follows:

import StoreKit

SKCloudServiceController.requestAuthorization { status in

}

Note that this call will crash your app if you don't have the NSAppleMusicUsageDescription key in your Info.plist.

To check if your app is authorized, you can check SKCloudServiceController.authorizationStatus() == .authorized.

The "request permission" workflow in Streaks Workout.

The above screenshot shows how we present Apple Music to the user. Once they tap OK, we can check if they have an Apple Music account and show options accordingly.

Check For An Apple Music Account

You can only play music from Apple Music if the user has an account. Once you've requested access to their music library, you can check the user's capabilities.

import StoreKit

let controller = SKCloudServiceController()

controller.requestCapabilities { capabilities, error in 
    if capabilities.contains(.musicCatalogPlayback) {
        // User has Apple Music account
    }
}

Once you've confirmed they have an account, you can attempt to play music.

Sign Up To Apple Music

If the user doesn't have Apple Music, you can check if they're eligible to sign up. If so, StoreKit provides UI components to facilitate this process.

Building upon the previous example, check if they're eligible to sign up:

import StoreKit

let controller = SKCloudServiceController()

controller.requestCapabilities { capabilities, error in 
    if capabilities.contains(.musicCatalogPlayback) {
        // User has Apple Music account
    }
    else if capabilities.contains(.musicCatalogSubscriptionEligible) {
        // User can sign up to Apple Music
    }
}

You can display the sign-up screen as follows. Note that if you have Apple affiliate account, you can include your affiliate token to get credit for the sale if the user signs up.

import StoreKit

class YourViewController: UIViewController {
    // ...
    
    let affiliateToken = "..."
    
    func showAppleMusicSignup() {
        let vc = SKCloudServiceSetupViewController()
        vc.delegate = self

        let options: [SKCloudServiceSetupOptionsKey: Any] = [
            .action: SKCloudServiceSetupAction.subscribe,
            .affiliateToken: affiliateToken,
            .messageIdentifier: SKCloudServiceSetupMessageIdentifier.playMusic
        ]
            
        vc.load(options: options) { success, error in
            if success {
                self.present(vc, animated: true)
            }
        }

    }

In addition to the .playMusic option, there is also .join, .connect, and .addMusic. Changing this will slightly adjust the wording and layout of the signup screen.

How the Apple Music sign-up screen appears in your app.

You can also implement the delegate method for this view controller to find out when the screen was closed:

extension YourViewController: SKCloudServiceSetupViewControllerDelegate {
    func cloudServiceSetupViewControllerDidDismiss(_ cloudServiceSetupViewController: SKCloudServiceSetupViewController) {
        // ...
    }
}

Playing an Item From Apple Music

At this stage, assuming the user has a valid Apple Music account, you can play the item you found in the earlier search request.

import MediaPlayer

let storeIds: [String] = [ "ID from earlier" ]

let player = MPMusicPlayerController.applicationQueuePlayer
let queue  = MPMusicPlayerStoreQueueDescriptor(storeIDs: storeIds)

player.setQueue(with: queue)
player.play()

As you can see, the actual playback was probably the easiest part!

Note that there are a number of different ways of doing certains things (many different API calls, different ways of achieving music playback, depending on your needs), but I've tried to keep it as straightforward as possible in this article.

It is not the most obviously function to implement in your app, but hopefully by laying out my experiences with Apple Music it makes the process simpler for you!