MusicKit and App Clips
Helm Pro yearly subscribers now get a 30% discount on RocketSim thanks to contingent pricing on the App Store.
Last week I published an article about how to create an App Clip for your app where I mentioned that App Clips have a set of limitations such as the amount of memory they can use and the amount of permissions they can request.
While I was working on the NowPlaying App Clip, I faced one of these limitations and I thought it would be interesting to share my experience with you and how I got around it.
The problem
The NowPlaying App Clip, which is still in the early stages of development, shows information about a given song using the same layout and data as the main app.
To find the song from an ID, such as an ISRC (International Standard Recording Code), NowPlaying uses MusicKit, which is an Apple framework to interact with Apple Music. Unfortunately for us, we couldn’t reuse our logic from the main app as is, as MusicKit does not work in App Clips.
Before going into the details about why it’s not possible to use MusicKit in an App Clip, let’s set some context about how I set up the App Clip and what I tried to get it working.
Setting up MusicKit for the App Clip
The first thing you need to do to be able to use MusicKit in your app is to create an app identifier with the same bundle identifier as your App Clip in your Developer Account and enable the MusicKit App Service:
Adding keys to the Info.plist
As MusicKit requires authorization from the user and will present an alert, you need to add the ‘Privacy - Media Library Usage Description’ (NSAppleMusicUsageDescription
) key with a description of why you need access to the user’s media library to your target’s Info.plist
file.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSAppleMusicUsageDescription</key>
<string>Need access to your music library</string>
</dict>
</plist>
Requesting authorization
Now that you have added the services to the App Clip identifier in App Store Connect and the NSAppleMusicUsageDescription
key to the Info.plist
file, you can go ahead and request authorization from the user in your App Clip’s code:
import SwiftUI
import MusicKit
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
.task {
guard await isAppAuthorised() else {
return
}
}
}
private func isAppAuthorised() async -> Bool {
guard MusicAuthorization.currentStatus != .authorized else {
return true
}
let response = await MusicAuthorization.request()
return response == .authorized
}
}
It doesn’t work 😭
If you run your App Clip target now, you will see that, regardless of what you try, the permission status will always be .denied
and the request
method will never present the authorization alert to the user.
I have not been able to find any documentation about this but, after asking a few people about this issue, I found out that it’s not possible to grant MusicKit permissions in an App Clip.
What I tried
I was keen on getting the App Clip working so I decided to persevere and try a few different things to see if I could work around the permissions issue.
❌ Catalogue requests
As the App Clip only needs to find a song by its ISRC (International Standard Recording Code) directly from the Apple Music catalogue without the need to access the user’s media library, I thought that I could potentially use this part of the MusicKit
framework without requesting authorization:
import SwiftUI
import MusicKit
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
.task {
do {
let isrc = "QM72823682"
let songRequest = MusicCatalogResourceRequest<Song>(matching: \.isrc, equalTo: isrc)
let songResponse = try await songRequest.response()
print(songResponse.items.first)
} catch {
print(error.localizedDescription)
}
}
}
}
Despite my high hopes, this didn’t work either. As soon as I ran the App Clip, I got the following error:
Permission denied
Failed to fetch current country code because the music authorization status is set to .denied. This is recoverable by guiding your user to the privacy settings, so they can grant your app access to Apple Music.
❌ Making a request to the Apple Music API using MusicKit
I wasn’t ready to give up at this point and I decided to request the information from the Apple Music API directly. If you have ever tried to use this API, you will know that authenticating with it manually can certainly be cumbersome. Thankfully, MusicKit offers a way of making authenticated network requests to the Apple Music API with very little effort:
import SwiftUI
import MusicKit
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
.task {
do {
let isrc = "QM72823682"
let urlString = "https: //api.music.apple.com/v1/catalog/us/songs?filter[isrc]=\(isrc)"
let url = URL(string: urlString)!
let songRequest = MusicDataRequest(urlRequest: .init(url: url))
let response = try await songRequest.response()
let items = try JSONDecoder().decode (MusicItemCollection<Song>.self, from: response.data)
print(items.first)
} catch {
print(error.localizedDescription)
}
}
}
}
Unfortunately, this approach didn’t work either. Despite this request only needing a developer token to work, I still got the same permission error as before:
Permission denied
Failed to request tokens because the music authorization status is set to .denied. This is recoverable by guiding your user to the privacy settings, so they can grant your app access to Apple Music.
✅ Making a request to the Apple Music API using URLSession
Finally, I opted to make the request to the Apple Music API myself using URLSession
and manual authentication. For this specific request, as it doesn’t need any user permissions, I only had to get a developer token and add it to the request’s headers.
MusicKit does most of the heavy lifting of creating a developer token for you and allows you to retrieve it with a single line of code:
let developerToken = try? await MusicDataRequest.tokenProvider.developerToken(options: .ignoreCache)
However, and as you might be suspecting by now, this doesn’t work in an App Clip either. Unfortunately, when you try to get the developer token, you get the following error saying permission is denied 😭:
Failed retrieving developer token: Error Domain=ICError Code=-7010 "Failed to get listener endpoint for cloud service status monitor." UserInfo={NSDebugDescription=Failed to get listener endpoint for cloud service status monitor., NSUnderlyingError=0x600000c754d0 {Error Domain=NSCocoaErrorDomain Code=4097 "connection to service named com.apple.itunescloudd.xpc" UserInfo={NSDebugDescription=connection to service named com.apple.itunescloudd.xpc}}}. Throwing .permissionDenied.
Generating a developer token
With my options very limited at this point, and as I wasn’t quite ready to give up yet, I decided to go ahead and generate a developer token manually.
Contrary to what you might think, this process is fairly complex and, over the next few sections, I will do my best to walk you through the process.
Creating a Media ID
The first thing you need to do is to create a new media identifier in your Developer Account by going to the ‘Identifiers’ section, clicking on the +
button, selecting ‘Media IDs’ from the list and clicking on ‘Continue:
Next, you need to select ‘MusicKit’ from the list, give the Media ID an identifier and a description and click on ‘Continue’:
Finally, register the new Media ID:
Creating a new key
Once you have done this, head over to the ‘Keys’ section in the Apple Developer Portal and click on the ’+’ button:
Give your key a name and enable ‘Media Services’ from the list:
Tap on ‘Configure’ next to the ‘Media Services’ row, select the Media ID you created earlier and click ‘Save’:
Finally, click on ‘Continue’ and then finish the process by downloading the key. You must keep this key somewhere safe as you won’t be able to download it again.
You will also need to get the key id from the key’s details page:
Creating a JWT token
Now that you have a media id and a key, you need to turn them into a JWT token that you can use to authenticate with the Apple Music API.
I decided to do this using Swift with the SwiftJWT library, a Swift package that helps you create and verify JWT tokens.
You can find more information about the specifics of the JWT token’s contents in Apple’s documentation, but to make a compatible JWT token from the id and key you created earlier, you can use the following code:
import SwiftJWT
struct JWTClaims: Claims {
let iss: String
let iat: Date?
let exp: Date?
}
func generateToken(privateKey: String) throws -> String {
let header = Header(kid: "key-id")
let claims = JWTClaims(iss: "asc-team", iat: Date(), exp: Date() + 60 * 60 * 12)
var jwt = JWT(header: header, claims: claims)
let keyData = key.data(using: .utf8)!
return try jwt.sign(using: .es256(privateKey: privateKey.data(using: .utf8)!))
}
🌟 Full credit for the code snippet above goes to this awesome YouTube video by Get Swifty, who does an amazing job at explaining how to make a JWT token for the Apple Music API.
Note that the privateKey
parameter in the method above is the contents of the key you downloaded earlier. You can view and copy the key’s contents by opening it in a text editor of your choice.
I would recommend keeping this key safe and making the JWT token short-lived to increase security. In the example above, I have set the token to expire after 12 hours.
You can always verify that the token is valid by extracting the claims from the raw JWT string before making a request using SwiftJWT:
import SwiftJWT
func isTokenValid(tokenString: String) -> Bool {
guard let expiry = try? JWT<JWTClaims>(jwtString: token).claims.exp else {
return false
}
return expiry > Date()
}
Decorating the request
Now that you have a JWT token, you are ready to make a request to the Apple Music API and use the response to display information in your App Clip.
All you have to do now is write some URLSession
code that makes a request to the Apple Music API and decorate it with the JWT token you created earlier:
import SwiftUI
import MusicKit
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
.task {
do {
let isrc = "QM72823682"
let urlString = "https://api.music.apple.com/v1/catalog/us/songs?filter[isrc]=QM7282368269"
let url = URL(string: urlString)!
let session = URLSession.shared
let (data, _) = try await session.data(for: appleMusicRequest(for: url))
let decoder = JSONDecoder()
// A struct conforming to `Decodable`
let songResponse = try decoder.decode(SongResponse.self, from: data)
print(items.first)
} catch {
print(error.localizedDescription)
return nil
}
}
}
private func appleMusicRequest(for url: URL) -> URLRequest {
let keyToken = "🙈"
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(keyToken)", forHTTPHeaderField: "Authorization")
return request
}
}