iOS Programming · · 31 min read

Using MusicKit and Apple Music API to Build a Music Player

Using MusicKit and Apple Music API to Build a Music Player

Hey everyone and welcome back to the second and final part of this tutorial series where we explore the intricacies of Apple’s MusicKit by building our very own music player in SwiftUI that can stream songs from our Apple Music account. If you haven’t read Part 1, you can do that here.

In the last tutorial, we looked at how to create a MusicKit Identifier for our Apple Developer account, created a JSON Web Token private key, and we were able to successfully make web requests to the Apple Music API. At the end of the tutorial, we also built the UI of our music player and were able to make it look like below.

In this tutorial, we’ll start making calls to the Apple Music API to populate our app with real data. We’ll also look at how to control media playback with the MediaPlayer framework and enhance the app to this.

musickit-player-42

Cool, right? Let’s get started.

Note: This tutorial was made using Xcode 11.4 and Swift 5.1. An active Apple Music subscription will be needed in order to test the app out. You will also need to run your app on a real device because as of this time, the Simulator does not support Apple Music playback.

Apple Music API vs iTunes Search API

If you’ve worked with podcasts, tv shows, or any other Apple generated media content, you must have come across the iTunes Search API. The iTunes Search API lets you search for content in the iTunes Store, App Store, and iBooks Store. This will give you access to information about apps, books, movies, podcasts, music, music videos, audiobooks, and TV shows. The best part about using this API is that it’s completely free so you can get updated information, on the fly, for no additional cost.

But why do we use the Apple Music API instead of the iTunes Search API? This is because of how much information and power the Apple Music API can deliver. While the Apple Music API provides the same functionality as the iTunes Search API (fetching information about music and music videos), the Apple Music API takes it to the next level by accessing a user’s personal library and recommendations.

This means your app can leverage a user’s playlists, songs, or ratings for their content. This is why it’s required for your user to have an Apple Music account if you will be using the API. Another important reason why the Apple Music API is better over the iTunes Search API is because it is built to work harmoniously with the MediaPlayer framework. As you’ll see, streaming audio is a breeze with the Apple Music API. Let’s get started!

Creating our Classes

Due to the nature of SwiftUI, making URL calls within our structs can get really complicated. Furthermore, it can get pretty complicated to populate our UI with different information gathered from different songs. This is why we’ll create an AppleMusicAPI class that will contain the functions we’ll frequently use in our app and a Song structure to make it easier to populate our UI.

Before we begin, download SwiftyJSON.swift and add the file to your project. This file contains the code for the popular library SwiftyJSON. This is a tremendous tool when making HTTP requests and receiving and parsing JSON responses. You can find more information about SwiftyJSON here.

To begin, choose to File > New > File. From the popup that appears, select Swift file. Name this file AppleMusicAPI.swift and save this file in the default location. You should now have a template file as shown below.

musickit-api-xcode

Similarly, let’s create a struct for our songs. Go to File > New > File and select Swift file. Name this file as Song.swift and save this file. We’ll begin creating our Song structure first.

Add the following code below import Foundation:

struct Song {
    var id: String
    var name: String
    var artistName: String
    var artworkURL: String

    init(id: String, name: String, artistName: String, artworkURL: String) {
        self.id = id
        self.name = name
        self.artworkURL = artworkURL
        self.artistName = artistName
    }
}

The code above creates a simple structure for Song. In case if you’re not familiar with structures, don’t worry. A structure is a type of class that helps make your code more concise, especially when dealing with structural data. Structures, just like classes, can define properties, methods, and initializers to set up their initial state. This is what we are doing with our init method. We are providing a default initializer method for the Song structure that lets a user input the ID, name, artist’s name, and album artwork’s URL.

Now switch over to the AppleMusicAPI.swift file. Here, we’ll be implementing a class called AppleMusicAPI that stores our developer token and a bunch of methods that will help when playing our song through our app.

Remove import Foundation and Add the following code to AppleMusicAPI.swift:

// 1
import StoreKit

// 2
class AppleMusicAPI {
    // 3
    let developerToken = "YOUR DEVELOPER TOKEN FROM PART 1"

    // 4
    func getUserToken() -> String {
        var userToken = String()

        return userToken
    }
}

The above code is pretty self-explanatory but don’t worry if this is a little unclear. Here’s what the above function does.

  1. First, we import the StoreKit framework. This helps us access a lot of built-in methods that can communicate with the Apple Music API.
  2. Here, you’ll notice we’re defining a class called AppleMusicAPI. This makes it easier to manage multiple instances of this class and reference the methods from other views in our app.
  3. Here we’re defining a constant developerToken containing the developer token we created in Part 1 on this tutorial series. This makes it easier to call the token when communicating with the API.
  4. Finally, we define our first method in this class: getUserToken(). As mentioned earlier, the Apple Music API can get a user’s library and playlists. This is only possible if we receive a token that is identifiable to a particular user.

Let’s finish implementing the rest of the getUserToken method.

func getUserToken() -> String {
    var userToken = String()

    // 1
    let lock = DispatchSemaphore(value: 0)

    // 2
    SKCloudServiceController().requestUserToken(forDeveloperToken: developerToken) { (receivedToken, error) in
        // 3
        guard error == nil else { return }
        if let token = receivedToken {
            userToken = token
            lock.signal()
        }
    }

    // 4
    lock.wait()
    return userToken
}

You might face some new code in the above snippet. Don’t worry though as most of it will be explained.

  1. First, we define a lock that is of type DispatchSemaphore. What is a DispatchSemaphore? A dispatch semaphore is an efficient implementation of halting a thread until a particular message has been passed. This “locks” a thread from executing more code until a signal has been given.
  2. We access the SKCloudServiceController().requestUserToken() method to get a token that authenticates the user in personalized Apple Music API requests. Notice how we use our constant developerToken in this method. It’s definitely easier than typing the long string again and again.
  3. Here, we write some code to error check what the requestUserToken() function returns. If receivedToken is not empty, we set it equal to our userToken variable from above. Notice, afterwards, we call lock.signal(). This lets the dispatch semaphore we create earlier know that it’s ok to start executing remaining code.
  4. Since this method executes asynchronously, it’s possible that when getUserToken() executes, it can skip to the line return userToken, even before a token is received from the SKCloudServiceController. By adding the lock.wait() line of code, this tells the code to halt executing any further code until a signal is given (as we implemented in Step 3).

Note: Dispatch sophomores must be used with caution. This can transform code to execute synchronously which can slow down an app or not update UI in time. In fact, if lock.signal is not called, the app will forever remain stuck until the user restarts the app. Thus, it’s not advised to use this in big production apps. However, for our purposes, it’s completely acceptable.

Now, let’s implement our next method: fetchStorefront().

A storefront is an object that represents the iTunes Store territory that the content is available in. When we perform a search using the Apple Music API, we’d like to show results relevant to our user’s location. Beneath, getUserToken(), add the following method:

func fetchStorefrontID() -> String {
    // 1
    let lock = DispatchSemaphore(value: 0)
    var storefrontID: String!

    // 2
    let musicURL = URL(string: "https://api.music.apple.com/v1/me/storefront")!
    var musicRequest = URLRequest(url: musicURL)
    musicRequest.httpMethod = "GET"
    musicRequest.addValue("Bearer \(developerToken)", forHTTPHeaderField: "Authorization")
    musicRequest.addValue(getUserToken(), forHTTPHeaderField: "Music-User-Token")

    // 3
    URLSession.shared.dataTask(with: musicRequest) { (data, response, error) in
        guard error == nil else { return }

        // 4
        if let json = try? JSON(data: data!) {
            print(json.rawString())
        }
    }.resume()

    // 5
    lock.wait()
    return storefrontID
}
  1. Just like earlier, we create a dispatch semaphore called lock to make sure that the function returns a storefrontID only after the data has been received from our URL request.
  2. We create a URLRequest using the Apple Music API url. A lot of the details about handling requests and responses from the Apple Music API can be found here, but here’s the gist. To compose a request to the API, first specify the root path, https://api.music.apple.com/v1/  . Here, we specify the storefront component. Using a GET Request, we create a request, adding our developer token in the header.
  3. Next, we use URLSession to send musicRequest. This is very similar to performing any networking requests for either obtaining images or data. As the .dataTask method is built, we are returned with three constants: data (the data the network response sends back), response (details about the response), and error(this may be nil if there is no error). After checking to make sure there is no error, we move to Step 4.
  4. The data we get is a JSON payload. A payload is the part of transmitted data that is the actual intended message. Using the SwiftyJSON library we added earlier, we’ll print the raw JSON output before parsing it. Notice that we’re not calling lock.signal() here because we’re not setting anything to the storefrontID. If we did call lock.signal(), our app would crash when testing it. We’ll add the signal when we parse our JSON result.
  5. Finally, we ask the dispatch semaphore to wait before returning the storefront’s ID.

This was a lot of code so now is a good time to make sure everything is working properly. We’ll switch to ContentView.swift and add an onAppear function to our TabView. Whatever code we place inside this method will be executed when TabView is displayed to the user. The code we’ll place inside will ask the user for permission to access their media library and upon authorization, will call the fetchStorefront() method which will print the JSON payload.

Let’s do it! At the top of the file, add import StoreKit and then make your TabView view look as such

TabView(selection: $selection) {
// Previous Code
    ...
}
.accentColor(.pink)
.onAppear() {
    SKCloudServiceController.requestAuthorization { (status) in
        if status == .authorized {
            print(AppleMusicAPI().fetchStorefrontID())
        }
    }
}

These few lines of code are easily comprehendible. We ask the SKCloudServiceController (remember, this is an object provided by StoreKit that determines the current capabilities of the user’s music library) for authorization to access a user’s media library. If the user authorizes this access, we run print(AppleMusicAPI().fetchStorefrontID()) which will print the JSON payload of the request.

Notice how we call out AppleMusicAPI class. Similar to ContentView in SwiftUI or ViewController in storyboard-based Swift, by adding the pair of parentheses after the class’s name, we are initializing it which gives us access to all its constants and methods, such as the fetchStorefrontID() method.

Now before we run the app to make sure everything is functioning as expected, we need to make a slight addition to our Info.plist. We need to add the following property that gives gives a description to the user what we need access to their music library for. The key is Privacy - Media Library Usage Description and the value can be any message you wish it to be that adequately informs users what the data will be used for.

Now we can run out app!

You’ll need to run the app on a physical device with an active Apple Music subscription. Unfortunately, the simulator does not have the Music app so accessing a user’s Apple Music account is near impossible. Having an active Apple Music subscription lets you test the features of your app on your account.

After authorizing the app to access your Apple Music account, you should wait for some time and expect an output that looks like the one below.

Don’t worry about the error messages stating something along the liens of “Unable to get the local account”. Beneath that, you’ll see a JSON string printed out. This goes to show that our code is working. Let’s go back to AppleMusicAPI.swift and finish the rest of the fetchStorefrontID() method. Delete the line print(json.rawString()) and replace it with the following

// 1
let result = (json["data"]).array!
let id = (result[0].dictionaryValue)["id"]!

// 2
storefrontID = id.stringValue

// 3
lock.signal()
  1. We set result to be the array the JSON payload provides underneath the data key. id is simply value provided under the id key in the dictionary provided in result. This can sound a little confusing. The names of keys and values can be derived from the JSON string we printed earlier. To learn more about reading and parsing JSON, this [article][4] can help you.
  2. We set the storefrontID to the string value of the id constant from Step 1.
  3. Just like earlier, we signal the dispatch semaphore that it’s okay to execute remaining code and free up the thread.

Your entire fetchStorefrontID() method should look like this.

Run the app again and you’ll see how instead of printing a JSON file, only two characters are printed, signifying the storefront ID of your Apple Music account. For me, in the United States, a simple “us” is printed out.

If everything works, give yourself a pat on the back! We have one last method to implement in our AppleMusicAPI class and this one is pretty big. This method will allow us to search Apple Music’s entire library of 40 million+ songs based on the keywords given. Now’s a good time to take a break!

Music Search

Underneath our fetchStorefrontID() method, let’s add a new method called searchAppleMusic(searchTerm:) that will let us search Apple Music’s library based on the term. Here’s a boilerplate to get you started. You’ll notice it’s very similar to our fetchStorefrontID() function.

func searchAppleMusic(_ searchTerm: String!) -> [Song] {
    let lock = DispatchSemaphore(value: 0)
    var songs = [Song]()

    let musicURL = URL(string: "https://api.music.apple.com/v1/catalog/\(fetchStorefrontID())/search?term=\(searchTerm.replacingOccurrences(of: " ", with: "+"))&types=songs&limit=25")!
    var musicRequest = URLRequest(url: musicURL)
    musicRequest.httpMethod = "GET"
    musicRequest.addValue("Bearer \(developerToken)", forHTTPHeaderField: "Authorization")
        musicRequest.addValue(getUserToken(), forHTTPHeaderField: "Music-User-Token")

    URLSession.shared.dataTask(with: musicRequest) { (data, response, error) in
        guard error == nil else { return }

    }.resume()

    lock.wait()
    return songs
}

In terms of variables, we have our routine dispatch semaphore defined and a new songs array of type Song that we defined earlier. We’ll be storing the songs in this array and returning this array to populate our table. You’ll notice that searchTerm is a String provided as an input to this function. We also perform string manipulation on searchTerm to replace any instances of whitespaces with the + symbol as URL’s can’t contain any whitespaces.

We also create the URLRequest using the Apple Music API url. Notice that like before, it uses the root path https://api.music.apple.com/v1/. However, the URL component is catalog and we pass the search term as a parameter of the URL. Apart from these changes, we again use the GET request, create the request, and add our developer token to the header.

Now comes the important part which is parsing the data returned from our URLSession.shared.dataTask. Let’s think about what we need. We need to parse the data for an array of songs. We then need to isolate the different components of the each song in the array such as title, artist, album artwork, etc. After isolating these components, we can create an object of type Song and add this to our songs array. This is how it can be accomplished.

Type the following code and place it underneath guard error == nil else { return } inside the URLSession.shared.dataTask() method:

// 1
if let json = try? JSON(data: data!) {
    // 2
    let result = (json["results"]["songs"]["data"]).array!
    // 3
    for song in result {
        // 4
        let attributes = song["attributes"]
        let currentSong = Song(id: attributes["playParams"]["id"].string!, name: attributes["name"].string!, artistName: attributes["artistName"].string!, artworkURL: attributes["artwork"]["url"].string!)
        songs.append(currentSong)
    }
    // 5
    lock.signal()
} else {
    // 6
    lock.signal()
}

This is very similar to the code we wrote above to fetch a user’s storefront ID. The only difference it there’s a lot more JSON parsing and we are creating a Song object to add to our array.

  1. First, we type check to see is the data returned is in a valid JSON format. If it is, we create a constant called json containing all this data in the JSON file format.
  2. We parse json for the songs that is nested within results -> songs -> data in an array format. Now, I don’t want to burden you with more JSON formatting but this order was derived by printing the raw string of json and noticing how each component was ordered. If you want, you can print json to see how result is nested within the JSON data. result will now contain an array of our songs.
  3. Now, we parse through each song in result using our trusty for loop.
  4. First, we define a constant called attributes that is a dictionary of all the attributes of our current song. These attributes contain lots of information about the current song but we’re interested in the id, name, artist’s name, and artwork URL. Then we create a currentSong constant of type Song that is populated with the values from the attributes dictionary. Finally, we append the currentSong to our songs array.
  5. Last but not least, we signal our dispatch semaphore throughlock so the rest of the thread can be freed up to run the remaining code.
  6. This is more of a check but if the data returned is not in a valid JSON format, we don’t want our app to stay stuck forever. This is why we add an else clause to signal the dispatch semaphore to continue the rest of the code.

Now we need to make sure our function works. Head over to ContentView.swift. In the .onAppear() declaration, we need to make a very minor change to the statement where we print AppleMusicAPI().fetchStorefrontID(). All we need to do is replace .fetchStorefrontID() with .searchAppleMusic("Taylor Swift"). If all goes well, when we run the app, this will print an array of Song objects containing all the songs Apple Music returns when its catalog is searched for Taylor Swift. (Of course, you can replace this with any artist, album, song, or word of your choice).

Run the app and observe the console. After a few seconds, you should see an output similar to the following:

As you can see, a whole array containing the songs based on your search term. We can look at the artist name, id, name of the song, and a URL to the album artwork. Since we know our code works, let’s delete the entirety of the .onAppear() declaration. That way we don’t necessarily make calls to the API when our app loads.

Now comes the second part of this section which is populating our search table view with the results.

Populating the Table

Go to SearchView.swift. We’ll be making all the changes here to populate our search table. There are many changes we have to make so follow carefully.

First, we need to replace the songs array containing some temporary strings. Delete that line and replace it with the following line:

@State private var searchResults = [Song]()

This creates a new empty array called searchResults that conforms to our Song object. We also link it to the @State property so anytime any changes are made to this array, any UI using this variable will be updated.

Now, as expected, you’ll get some errors since we removed our old songs array. Let’s start at the ForEach within our List SwiftUI component. The error displayed here is that we’re still trying to iterate over a variable songs that doesn’t exist. Replace the ForEach line with the following:

ForEach(searchResults, id:\.id) { song in
    ...

There are three changes we’ve made here.

  1. The first change is we’ve replaced songs which searchResults.
  2. Now since our Song object doesn’t conform to the Hashable property, we can’t use .self to use it as an id. Good news though! Our Song objects have an id property to identify each element. This is why we replace \.self with \.id.
  3. Finally, and this is really minor, we replace the songTitle variable to song. songTitle referred to the string from our old array. This isn’t very representative of the current array for which we are iterating which is why we rename it to song.

While this error is removed, we now have errors wherever we used the variable songTitle. This is a simple fix as we replace songTitle with song.name. Modify the code after the Image object within your HStack to look like the following:

VStack(alignment: .leading) {
    // 1
    Text(song.name)
        .font(.headline)
    // 2
    Text(song.artistName)
        .font(.caption)
        .foregroundColor(.secondary)
}
Spacer()
Button(action: {
    // 3
    print("Playing \(song.name)")
}) {
    Image(systemName: "play.fill")
        .foregroundColor(.pink)
}
  1. first change we made was to replace songTitle with song.name. This will use the name of the song as the string value for this Text object.
  2. The second change we made is to replace the ”Artist Name” placeholder with the actual artists name. Just like song.name, we use song.artistName to populate this Text object with the value in song.artistName.
  3. Finally, the last change we made was a slight modification to our Button object which is to replace songTitle with song.name just like Step 1.

Now, you can build and run the app. Everything should compile and work as normal but there’s a catch. You’ll notice that when you search for a song and press enter, nothing happens. This is because we still haven’t called the searchAppleMusic function from our AppleMusicAPI() class. To do this, we need to make some changes to our TextField. Before this, at the top of the file underneath import SwiftUI, add the line import StoreKit.

Now, make the following changes to your TextField:

TextField("Search Songs", text: $searchText, onCommit: {
    // 1
    UIApplication.shared.resignFirstResponder()
    if self.searchText.isEmpty {
        // 2
        self.searchResults = []
    } else {
        // 3
        SKCloudServiceController.requestAuthorization { (status) in
            if status == .authorized {
                // 4
                self.searchResults = AppleMusicAPI().searchAppleMusic(self.searchText)
            }
        }
    }
})
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(.horizontal, 16)
.accentColor(.pink)

We’re at the last stretch here but this code shouldn’t be too complicated for you to read. Here’s a gist of what it compiles:

  1. First, we’d like to dismiss the keyboard when the user presses Search on the keyboard. This is done by calling the line UIApplication.shared.resignFirstResponder() .
  2. Next, we’d like to make sure that if the user doesn’t enter anything, our searchResults are empty. This is why we set it to an empty array if the searchText is empty.
  3. If the searchText variable is not empty, we’d like to call our searchAppleMusic function. Before we do this, we make sure that SKCloudServiceController has the necessary authorization to access a user’s media library. If the user authorizes this access, we can continue to the next line of code.
  4. Finally, we set out searchResults array equal to the results of AppleMusicAPI().searchAppleMusic(self.searchText). This will pass the search term to our searchAppleMusic function and update our searchResults array with whatever Apple Music returns!

We’re finally ready to run our app. Build and run the app and head over to the Search page. Enter any term, wait for a couple seconds as your app makes network requests you’ve implemented, and watch with delight as the table view is populated with the results of your search term.

Congratulations! You’ve mastered the basics of the Apple Music API! As you can see, it’s very repetitive in that the steps can be summed down to the following:

  1. Make a network call to the Apple Music API
  2. Parse the JSON to see where the data you need can be found
  3. Create an object and populate it with the data you’ve parsed

For the remainder of the tutorial, we’ll be focusing on the under appreciated MediaPlayer framework and its very important object, MPMusicPlayerController . Also, don’t worry about the album artwork not displaying as of now. I’ll show you how we can use Swift Package Manager to successfully load and display images from a URL.

Implementing Music Play Back

Get excited because now we’ll be looking at the MediaPlayer framework and how to access and control your device’s music player. Essentially, the MediaPlayer framework is a part of MusicKit and lets you control playback of the user’s media from your app.

To play content using this framework, we have to instantiate one of the built-in MPMusicPlayerController objects. There are two types. Here is a brief description of what they are:

  1. System Player: This media player is directly linked to the Music app on your device. If you choose to use this player, then when a user click on a song, it will open up Music and start playing from that app.
  2. Application Player: This will be a media player that is built-in directly to your app. That way when a user clicks on a song, they won’t be transported to the Music app, but rather all the playback functionality will occur in your app. Of course, this means that you will have to add play/pause and skip/rewind functionality. We’ll be using this type of player for our app.

Let’s add the application player to our app. Now we want to modify this object within both screens of our app. In our Player View, we want to control the playback of the currently playing item in the song and in our Search View, we want to be able to play songs directly from that page. As such, we’ll be implementing @State and @Binding protocols to help with data flow.

Head over to ContentView.swift. At the top of the file, type import MediaPlayer underneath import StoreKit. Next, add the following line where you declare your selection variable:

@State private var musicPlayer = MPMusicPlayerController.applicationMusicPlayer

We’re creating a variable called musicPlayer that is of the type applicationMusicPlayer as we discussed above. We’re also adding the @State property to it so we can use it to pass it to other views. Let’s do that right now!

Go to PlayerView.swift and at the top of the file, add import the MediaPlayer framework with:

import MediaPlayer

Next, at the top of thestruct, add the following line:

@Binding var musicPlayer: MPMusicPlayerController

This creates a Binding variable for our Player View. Since we will be binding this variable to the musicPlayer we created in ContentView.swift, anytime we make a change in Player View to this object, it will be reflected in Content View.

Since we can’t pass this binding variable to our PlayerView_Previews and SwiftUI previews have no support for MusicKit, there’s no point in having this in our file anymore. Delete the entire PlayerView_Previews structure from this file.

Let’s do the same thing in SearchView.swift. You should already have the MediaPlayer framework implemented. Just like before, add the following line to the top of your SearchView structure below the initialization of the searchResults array.

@Binding var musicPlayer: MPMusicPlayerController

Similar to PlayerView.swift, delete the entire SearchView_Previews structure from this file.

Now, head back to ContentView.swift. You should see some errors for PlayerView() and SearchView(). This is simply because we haven’t added the argument for the parameter musicPlayer when we call it. You can fix this by changing PlayerView() to PlayerView(musicPlayer: self.$musicPlayer) and SearchView() to SearchView(musicPlayer: self.$musicPlayer).

Build the app! It should compile without errors. We now have a built-in media player that is accessible to all the views in our app! Now when we implement the play and pause methods, we can easily update our player and reflect those changes across all the views in our app!

Playing Music

When our user clicks on a song from the search table, we’d like our app to play the song selected. This means that we have to add a song to our musicPlayer’s queue and tell the queue to play. Watch how we implement this in two line!

Go to SearchView.swift. Find the Button structure that should be inside the List structure. As of right now, it only prints the name of the song it is playing to the console. Delete that line and replace it with the following:

self.musicPlayer.setQueue(with: [song.id])
self.musicPlayer.play()

That’s it! In the first line, we set the queue of our musicPlayer. The queue in this case is an array of strings containing the ID’s of the song. This is why the MediaPlayer and Apple Music API work harmoniously together. The framework can automatically detect which song is to be played based on the ID given. The final line simply instructs our musicPlayer to play whatever is in the queue!

Build and run the app! You should be able to search for a song, and when you tap on it, your music player should start playing the song!

If you’ve got everything working, congratulations! This is one of the most under appreciated, unknown functionalities you can build using Swift! We’re not done yet though! While we can play music, we’d also like to control the playback state of the song! We also want to make sure that the Player View can show us the name of the song and tag artist’s name. This can be achieved relatively simply as well!

Go to PlayerView.swift. Let’s start at the top of the file and make the respective changes as we work our way down!

Find the VStack structure where we define our Text objects that will contain the name of the song and the artist’s name. Replace it with the following:

VStack(spacing: 8) {
    Text(self.musicPlayer.nowPlayingItem?.title ?? "Not Playing")
        .font(Font.system(.title).bold())
    Text(self.musicPlayer.nowPlayingItem?.artist ?? "")
        .font(.system(.headline))
}

The changes we’ve made here is to replace “Song Title” and “Artist Name“ with the actual name of the currently playing song and the artist by whom it’s sung. If no song is playing, then we leave the artist’s name empty and set the name of the song to ”Not Playing”.

Before we go further, I’d like to explain a little bit about this .nowPlayingItem object we’re calling. This object is of a type called MPMediaItem. This object is provided by the MediaPlayer framework and is an object which contains a collection of properties that represents what you could call a “song” in a media library. More specifically, an MPMediaItem can be any object that can be played and has a metadata that is similar to any audio-based object such as a song or podcast. The properties we’re using here are title, which is the name of the song, and artist, which is the name of the artist. There are many more properties that you can use if you choose to that can be found here.

Build and run your app. You can see that when you start to play a song, the metadata is displayed where it’s supposed to be!

musickit-demo-app-ui

Now, let’s add some play and pause functionality. Scroll down in PlayerView.swift and go to the Button object which currently prints ”Pause” to the console. Delete this line and replace it with the following:

if self.musicPlayer.playbackState == .paused || self.musicPlayer.playbackState == .stopped {
    self.musicPlayer.play()
} else {
    self.musicPlayer.pause()
}

Our musicPlayer has a property called playbackState that is of type MPMusicPlaybackState. This property keeps track of whether the music player is playing a song, paused, or stopped. If the playbackState is equal to.paused or .stopped, then nothing is playing and when the user clicks on this button, we’d like for it to start playing. This is why we call self.musicPlayer.play(). Similarly, in the else clause, we call self.musicPlayer.pause() because we’d like to stop the currently playing song.

You can build and run the app on your device. You should see that when you click on the pause button, the music pauses and when you press again, the music resumes. However, the icon of the button is not updating. This can be done by tracking the playback state of the currently playing item!

At the top of PlayerView.swift, underneath where you declared musicPlayer, type the following line:

@State private var isPlaying = false

This is a Boolean variable that is attached to the @State property that will be updated based on the playback state of the currently playing song. First, go to the Button object we modified above. Modify the action of the Button to look like the following:

if self.musicPlayer.playbackState == .paused || self.musicPlayer.playbackState == .stopped {
    self.musicPlayer.play()
    // 1
    self.isPlaying = true
} else {
    self.musicPlayer.pause()
    // 2
    self.isPlaying = false
}

We’ve added two lines that are pretty self explanatory. When we signal the musicPlayer to play, we set the isPlaying variable to true. When we pause the musicPlayer, we once again set the isPlaying variable to false.

Next, we need to use the isPlaying variable to update the image used as the logo for the button. This is quite simple. Underneath the button’s action where we declare the UI of the button, modify the ZStack object to look as such:

ZStack {
    Circle()
        .frame(width: 80, height: 80)
        .accentColor(.pink)
        .shadow(radius: 10)
    Image(systemName: self.isPlaying ? "pause.fill" : "play.fill")
        .foregroundColor(.white)
        .font(.system(.title))
}

The change we made here is to change the systemNameof the Image object used based on whether self.isPlaying is true or false. If it is true, we’ll use the pause.fill SF Symbol. If not, we’ll use the play.fill symbol.

Last but not least, we need a way to update self.isPlaying if there is a change made in Search View. For example, if our player is paused but we play a song from Search View, we’d like to update self.isPlaying by setting it to true. Now, we can use the @Binding protocol but I’d like to show you an easier alternative: the onAppear() function!

At the very bottom of PlayerView.swift, find the second to last } closing curly bracket. This is the bracket that will close our GeometryReader. Attach the .onAppear() function below it as shown:

.onAppear() {
    if self.musicPlayer.playbackState == .playing {
        self.isPlaying = true
    } else {
        self.isPlaying = false
    }
}

What are we doing in this function? Well, remember that our musicPlayer is already bound to all the views in our app. So any updates to this object is immediately reflected in every view. When we play a song from Search View, the musicPlayer is updated to set its playbackState to .playing. This is why in the .onAppear() function, we check to see that if the playbackState is in fact playing, then we set isPlaying to true. If not, we set isPlaying to false!

That’s all for playing and pausing! Build and run your app! You should see that the play/pause button accurately changes based on whether the song is currently playing or is paused!

Implementing Skip & Rewind

Now let’s implement the logic for skip and rewind buttons. This won’t take too long. Let’s start with the skip button.

Navigate to PlayerView.swift and locate the Button object that we’re using as a temporary skip button. It should be printing ”Skip” to the console when tapped upon.

Replace the line print("Skip") with self.musicPlayer.skipToNextItem() and that’s all! Build and run your app. When you press the skip button, it will jump to the next song in the queue, which is nothing, so the player will stop!

You can see how MediaPlayer comes with a list of functions that make it easy for us to control playback! With one line of code, we were able to implement a “skip song” functionality into our music player app. The “rewind” button is a little more challenging.

Think about your favorite music player. When you press the rewind button, what happens? If the song is more than 5 seconds through its playback, it skips to the beginning of the song. If the current playback is still within the first 5 seconds, then it goes to the previous song. With a simple if-else statement, watch how we can add the same functionality to our app.

Scroll back up to the Button object that currently prints ”Rewind” to the console. Delete that line and replace it with the following:

if self.musicPlayer.currentPlaybackTime < 5 {
    self.musicPlayer.skipToPreviousItem()
} else {
    self.musicPlayer.skipToBeginning()
}

You can see that we use a new property here: currentPlaybackTime. This is the length of the song, in seconds, that has been played by our player. If the number of seconds played so far is less than 5, we call upon a new method: skipToPreviousItem(). This method, provided by the MediaPlayer framework, we jump back in the queue and start playing the song before this song. Currently, this will continue playing the beginning of this song.

However, what we can test is that if currentPlaybackTime is greater than or equal to 5, we can call skipToBeginning() in our musicPlayer and that will ask our musicPlayer to start playing from the beginning of our current song.

Build and run your app! It should be able to handle the skip and rewind buttons as you implemented! As you can see from this section, a lot of the methods we use to control playback are already implemented in the MediaPlayer framework. With a little research, you can continue to build upon all these features.

Implementing Album Artwork

Finally, let’s replace the ugly “A” SF Symbol with the album artwork of the current song playing. If you remember from earlier, our Song object has a property called artworkURL that contains the string of a URL containing the image of the album.

Normally, we’d have to write a lot of code to process the URL, gather the image data, and cache the image so we wouldn’t make redundant calls. I’d like to show you a nifty Swift package called [SDWebImageSwiftUI][6]. This package contains a framework built for SwiftUI that can help with image loading and contains features like async image loading, memory/disk caching, animated image playback and more. If you ever need to load images from a URL in your apps, I highly recommend this framework since it automatically handles image loading and caching, thereby speeding up your app.

Here’s how we can install it. We’ll be using Swift Package Manager to link this framework with our project. In Xcode, go to the Title Bar, and click on File > Swift Packages > Add Package Dependancy.

A popup will show up and ask you to enter a package repository URL. The URL we will be using is https://github.com/SDWebImage/SDWebImageSwiftUI. Enter this URL and press Next.

After some loading, Xcode will ask you which version to use. Don’t make any changes and simple press Next. Xcode will take some time to fetch the repository and create a package to add to your project. After a minute, Xcode will show you a popup confirming you to choose the packages and target. Make sure it looks like something below.

The important point to follow here is that MusicPlayer is selected as the target. Click Finish and Xcode will take you to the main project page that shows all your Swift Packages. This means that the package has successfully been added to your project and you can start using the framework!

Go to SearchView.swift and at the top of the file, type import SDWebImageSwiftUI. This will gives us access to all the objects and methods in this framework.

In our List object, locate the Image object that is currently coded as such:

Image(systemName: "rectangle.stack.fill")
    .resizable()
    .frame(width: 40, height: 40)
    .cornerRadius(5)
    .shadow(radius: 2)

Making sure that all its modifiers are still in place, replace it with the following

WebImage(url: URL(string: song.artworkURL.replacingOccurrences(of: "{w}", with: "80").replacingOccurrences(of: "{h}", with: "80")))
    .resizable()
    .frame(width: 40, height: 40)
    .cornerRadius(5)
    .shadow(radius: 2)

SDWebImageSwiftUI provides an object called WebImage. This is very similar to our Image object but has some really neat additions. For one, it takes an argument called url which when provided, automatically loads the image from this URL.

We create a URL object and pass the string song.artworkURL. Notice that once again, we perform string manipulation here. If you notice the URL, you’ll notice that it has two parameters: {h} and {w}. These parameters stand for height and width, respectively. This is provided by the Apple Music API on purpose because we can specify the height and width that is best suited for us. I chose a height and width of 80, since this is 2x the size of our WebImage so the image loaded will have a high quality. Therefore, we call replaceOccurences on both {w} and {h} and replace it with 80.

Everything else should remain the same! Make sure your code looks like the screenshot below:

Build and run your app! Now, when you search for a song, you can actually see the album cover in the search results. The best part too is that SDWebImageSwiftUI caches the image so if you quit the app and search for the same term again, you can notice that there is a huge speed increase in loading the image.

Our last step is to change the album artwork in PlayerView.swift. Unfortunately, this is not as simple. See, in SearchView.swift we had a list of Song objects, each containing a URL to the album cover. Since we didn’t pass this list along, our Player View can’t access this album artwork URL. Furthermore, while MediaPlayer is great at providing many built-in objects and methods, it does not contain an object with a link to a particular song’s album artwork.

In order to fix this, we need to rely on State and Binding, once again. Head to ContentView.swift and at the top of the file, underneath where you created your musicPlayer variable, add the following file:

@State private var currentSong = Song(id: "", name: "", artistName: "", artworkURL: "")

We are creating a new shared object called currentSong of type Song that will be updated whenever we click on a new song from our Search View.

On that note, go to SearchView.swift and add the following Binding property at the top of the file, below where you declared your musicPlayer variable.

@Binding var currentSong: Song

Scroll to the bottom of the file and you’ll find the Button object that currently sets the queue of the musicPlayer and tells it to start playing. Over here, we want to set currentSong to the song that was tapped upon.

Modify the action of the Button as shown.

Button(action: {
    self.currentSong = song
    self.musicPlayer.setQueue(with: [song.id])
    self.musicPlayer.play()
})

All we’re adding here is setting the currentSong to the song that was tapped on.

Let’s do the same for PlayerView.swift. Navigate to that file and at the very top, add import SDWebImageSwiftUI. This will let us use the WebImage object as we did above.

Next, add the Binding variable currentSong to the structure, right below where we declared the isPlaying variable.

@Binding var currentSong: Song

Last but not least, let’s replace our current Image object which holds the SF Symbol a.square with the following code. Notice that all the modifiers remain the same.

WebImage(url: URL(string: self.currentSong.artworkURL.replacingOccurrences(of: "{w}", with: "\(Int(geometry.size.width - 24) * 2)").replacingOccurrences(of: "{h}", with: "\(Int(geometry.size.width - 24) * 2)")))
    .resizable()
    .frame(width: geometry.size.width - 24, height: geometry.size.width - 24)
    .cornerRadius(20)
    .shadow(radius: 10)

Now just like before, we pass a URL to WebImage, only this URL is very lengthy. Just like before, we are replacing {w} and {h} with the width and height we want. It’s not as simple as before however, since our height and width is reliant on the width of the device. This is why we replace {w} and {h} with geometry.size.width - 24. The number is actually of type Floatso to convert it to an integer, we wrap it around in Int. Finally, we multiply both lengths by 2 to get a sharper image quality.

Last but not least, we still need to pass the currentSong from our Content View to both the Player View and Search View. Head over to ContentView.swift and make the following changes to where you declare both PlayerView() and SearchView().

PlayerView(musicPlayer: self.$musicPlayer, currentSong: self.$currentSong)
...
SearchView(musicPlayer: self.$musicPlayer, currentSong: self.$currentSong)

In these changes, we passing the currentSong that was created in ContentView.swift to the remaining views.

Congratulations! This is the end of this lengthy tutorial! Build and run your app and watch with delight as your app can make calls to the Apple Music API, parse JSON and display a list of songs, utilize a devices media player capabilities to play songs and control playback, and harness external libraries to load images from remote sources- all using SwiftUI, a technology that is only a year old!

Conclusion

If you’ve reached the end successfully, congratulations! This two part tutorial was not your average, intermediate tutorial. Despite its lengthy content, I hope you learned something new from all the technologies we’ve utilized. I’ve compiled a list of resources below that can help you if you choose to continue building upon this app! You can download the completed project on GitHub.

For reference, you can check out the following resource related to this tutorial:

Apple Documentation and Videos

More on the Technology

As always, if you have any doubts, feel free to leave a comment below or reach me on Twitter @HeySaiK. I can’t wait to see what you’ll make with MusicKit!

Read next