iOS Spotify SDK — Swift 5 Tutorial

Ömer Fırat
9 min readMar 20, 2023

--

One of the subjects that I think plays a big role in our lives is music. That’s why I wanted to do a little application research about spotify. I decided that the resources were not very sufficient and I wanted to prepare a tutorial by myself. So Let’s start ! 👨🏼‍💻

1 - Create A New XCode Project

We will have to use the name of the application, so you can choose a name that you can work with more easily.

2 - Login to spotify developer website Here.

Then click on the dashboard from the menu at the top, you will see the create a app button on the page that comes up, after clicking on it, we need to enter our app name on the page that opens, but this name should be the same as the xcode file we created.

That’s all for now, we’ll be back here later !

3 - Install Spotify SDK from Here.

At the same time, let’s install the Spotify SDK file from the package dependencies section.

Then add ObjC Flag. Since many SDK is written in objective c library we have to do this and apply it in our swift file.

Then we need to add Objective-C File to our App.

You can give the name you want, then we will delete it.

This will then ask you if you want Xcode to create a bridging file for your. Say yes Create Bridging Header.

Then delete the objective c file and only the bridging header file remains and add #import <SpotifyiOS/SpotifyiOS.h> this code snippet.

4 - Setting up the info.plist

We can say that this process is the most important point required to use this SDK.

We need to set these settings in our developer app :

Bundle ID and Redirect URI

In p.list we need to set them :

URL Types, Queried URL Schemes and App Transport Security Settings.

5 - Let’s Configure Our App

Click the edit settings button in our spotify developer dashboard. Write your application name in the redirect uri and add “ :// ”. In the bundle id part, we copy and paste the bundle id in our xcode file.

In the end it should look like this.

After saving our settings, we move on to configuring our application in xcode.

6 - Setting up xcode

We open the demo application in the downloaded file.

We should copy and paste the parts I said into our own Xcode file. Which is URL Types, Queried URL Schemes and App Transport Security Settings.

After copy paste, our application should look like this. Our application name in Item 0 part. In the identifier part, it will be enough to enter the bundle id.

7- Building App

You can also check out Spotify’s own auth guide here.

Our application will need a refresh token in order to provide transactions, in this we need to get permission from our application first.

Let’s create a swift file named Constants and add this code init:

import Foundation

let accessTokenKey = "access-token-key"
let redirectUri = URL(string:"feeltune://")! // You will add your redirectURI.
let spotifyClientId = "yourClientId"
let spotifyClientSecretKey = "yourSecretKey"

/*
Scopes let you specify exactly what types of data your application wants to
access, and the set of scopes you pass in your call determines what access
permissions the user is asked to grant.
For more information, see https://developer.spotify.com/web-api/using-scopes/.
*/
let scopes: SPTScope = [
.userReadEmail, .userReadPrivate,
.userReadPlaybackState, .userModifyPlaybackState, .userReadCurrentlyPlaying,
.streaming, .appRemoteControl,
.playlistReadCollaborative, .playlistModifyPublic, .playlistReadPrivate, .playlistModifyPrivate,
.userLibraryModify, .userLibraryRead,
.userTopRead, .userReadPlaybackState, .userReadCurrentlyPlaying,
.userFollowRead, .userFollowModify,
]
let stringScopes = [
"user-read-email", "user-read-private",
"user-read-playback-state", "user-modify-playback-state", "user-read-currently-playing",
"streaming", "app-remote-control",
"playlist-read-collaborative", "playlist-modify-public", "playlist-read-private", "playlist-modify-private",
"user-library-modify", "user-library-read",
"user-top-read", "user-read-playback-position", "user-read-recently-played",
"user-follow-read", "user-follow-modify",
]

We need to add our own keys to the places in the code. You can find it in dashboard in Spotify Developer.

After creating the view controller, our application is ready to use and here is the View Controller code snippet :

//
// ViewController.swift
//

import UIKit

class ViewController: UIViewController {

// MARK: - Spotify Authorization & Configuration
var responseCode: String? {
didSet {
fetchAccessToken { (dictionary, error) in
if let error = error {
print("Fetching token request error \(error)")
return
}
let accessToken = dictionary!["access_token"] as! String
DispatchQueue.main.async {
self.appRemote.connectionParameters.accessToken = accessToken
self.appRemote.connect()
}
}
}
}

lazy var appRemote: SPTAppRemote = {
let appRemote = SPTAppRemote(configuration: configuration, logLevel: .debug)
appRemote.connectionParameters.accessToken = self.accessToken
appRemote.delegate = self
return appRemote
}()

var accessToken = UserDefaults.standard.string(forKey: accessTokenKey) {
didSet {
let defaults = UserDefaults.standard
defaults.set(accessToken, forKey: accessTokenKey)
}
}

lazy var configuration: SPTConfiguration = {
let configuration = SPTConfiguration(clientID: spotifyClientId, redirectURL: redirectUri)
// Set the playURI to a non-nil value so that Spotify plays music after authenticating
// otherwise another app switch will be required
configuration.playURI = ""
// Set these url's to your backend which contains the secret to exchange for an access token
// You can use the provided ruby script spotify_token_swap.rb for testing purposes
configuration.tokenSwapURL = URL(string: "http://localhost:1234/swap")
configuration.tokenRefreshURL = URL(string: "http://localhost:1234/refresh")
return configuration
}()

lazy var sessionManager: SPTSessionManager? = {
let manager = SPTSessionManager(configuration: configuration, delegate: self)
return manager
}()

private var lastPlayerState: SPTAppRemotePlayerState?

// MARK: - Subviews
let stackView = UIStackView()
let connectLabel = UILabel()
let connectButton = UIButton(type: .system)
let imageView = UIImageView()
let trackLabel = UILabel()
let playPauseButton = UIButton(type: .system)
let signOutButton = UIButton(type: .system)

// MARK: App Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
style()
layout()
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updateViewBasedOnConnected()
}

func update(playerState: SPTAppRemotePlayerState) {
if lastPlayerState?.track.uri != playerState.track.uri {
fetchArtwork(for: playerState.track)
}
lastPlayerState = playerState
trackLabel.text = playerState.track.name

let configuration = UIImage.SymbolConfiguration(pointSize: 50, weight: .bold, scale: .large)
if playerState.isPaused {
playPauseButton.setImage(UIImage(systemName: "play.circle.fill", withConfiguration: configuration), for: .normal)
} else {
playPauseButton.setImage(UIImage(systemName: "pause.circle.fill", withConfiguration: configuration), for: .normal)
}
}

// MARK: - Actions
@objc func didTapPauseOrPlay(_ button: UIButton) {
if let lastPlayerState = lastPlayerState, lastPlayerState.isPaused {
appRemote.playerAPI?.resume(nil)
} else {
appRemote.playerAPI?.pause(nil)
}
}

@objc func didTapSignOut(_ button: UIButton) {
if appRemote.isConnected == true {
appRemote.disconnect()
}
}

@objc func didTapConnect(_ button: UIButton) {
guard let sessionManager = sessionManager else { return }
sessionManager.initiateSession(with: scopes, options: .clientOnly)
}

// MARK: - Private Helpers
private func presentAlertController(title: String, message: String, buttonTitle: String) {
DispatchQueue.main.async {
let controller = UIAlertController(title: title, message: message, preferredStyle: .alert)
let action = UIAlertAction(title: buttonTitle, style: .default, handler: nil)
controller.addAction(action)
self.present(controller, animated: true)
}
}
}

// MARK: Style & Layout
extension ViewController {
func style() {
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 20
stackView.alignment = .center

connectLabel.translatesAutoresizingMaskIntoConstraints = false
connectLabel.text = "Connect your Spotify account"
connectLabel.font = UIFont.preferredFont(forTextStyle: .title3)
connectLabel.textColor = .systemGreen

connectButton.translatesAutoresizingMaskIntoConstraints = false
connectButton.configuration = .filled()
connectButton.setTitle("Continue with Spotify", for: [])
connectButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .title3)
connectButton.addTarget(self, action: #selector(didTapConnect), for: .primaryActionTriggered)

imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit

trackLabel.translatesAutoresizingMaskIntoConstraints = false
trackLabel.font = UIFont.preferredFont(forTextStyle: .body)
trackLabel.textAlignment = .center

playPauseButton.translatesAutoresizingMaskIntoConstraints = false
playPauseButton.addTarget(self, action: #selector(didTapPauseOrPlay), for: .primaryActionTriggered)

signOutButton.translatesAutoresizingMaskIntoConstraints = false
signOutButton.setTitle("Sign out", for: .normal)
signOutButton.titleLabel?.font = UIFont.systemFont(ofSize: 20, weight: .bold)
signOutButton.addTarget(self, action: #selector(didTapSignOut(_:)), for: .touchUpInside)
}

func layout() {

stackView.addArrangedSubview(connectLabel)
stackView.addArrangedSubview(connectButton)
stackView.addArrangedSubview(imageView)
stackView.addArrangedSubview(trackLabel)
stackView.addArrangedSubview(playPauseButton)
stackView.addArrangedSubview(signOutButton)

view.addSubview(stackView)

NSLayoutConstraint.activate([
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}

func updateViewBasedOnConnected() {
if appRemote.isConnected == true {
connectButton.isHidden = true
signOutButton.isHidden = false
connectLabel.isHidden = true
imageView.isHidden = false
trackLabel.isHidden = false
playPauseButton.isHidden = false
}
else { // show login
signOutButton.isHidden = true
connectButton.isHidden = false
connectLabel.isHidden = false
imageView.isHidden = true
trackLabel.isHidden = true
playPauseButton.isHidden = true
}
}
}

// MARK: - SPTAppRemoteDelegate
extension ViewController: SPTAppRemoteDelegate {
func appRemoteDidEstablishConnection(_ appRemote: SPTAppRemote) {
updateViewBasedOnConnected()
appRemote.playerAPI?.delegate = self
appRemote.playerAPI?.subscribe(toPlayerState: { (success, error) in
if let error = error {
print("Error subscribing to player state:" + error.localizedDescription)
}
})
fetchPlayerState()
}

func appRemote(_ appRemote: SPTAppRemote, didDisconnectWithError error: Error?) {
updateViewBasedOnConnected()
lastPlayerState = nil
}

func appRemote(_ appRemote: SPTAppRemote, didFailConnectionAttemptWithError error: Error?) {
updateViewBasedOnConnected()
lastPlayerState = nil
}
}

// MARK: - SPTAppRemotePlayerAPIDelegate
extension ViewController: SPTAppRemotePlayerStateDelegate {
func playerStateDidChange(_ playerState: SPTAppRemotePlayerState) {
debugPrint("Spotify Track name: %@", playerState.track.name)
update(playerState: playerState)
}
}

// MARK: - SPTSessionManagerDelegate
extension ViewController: SPTSessionManagerDelegate {
func sessionManager(manager: SPTSessionManager, didFailWith error: Error) {
if error.localizedDescription == "The operation couldn’t be completed. (com.spotify.sdk.login error 1.)" {
print("AUTHENTICATE with WEBAPI")
} else {
presentAlertController(title: "Authorization Failed", message: error.localizedDescription, buttonTitle: "Bummer")
}
}

func sessionManager(manager: SPTSessionManager, didRenew session: SPTSession) {
presentAlertController(title: "Session Renewed", message: session.description, buttonTitle: "Sweet")
}

func sessionManager(manager: SPTSessionManager, didInitiate session: SPTSession) {
appRemote.connectionParameters.accessToken = session.accessToken
appRemote.connect()
}
}

// MARK: - Networking
extension ViewController {

func fetchAccessToken(completion: @escaping ([String: Any]?, Error?) -> Void) {
let url = URL(string: "https://accounts.spotify.com/api/token")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
let spotifyAuthKey = "Basic \((spotifyClientId + ":" + spotifyClientSecretKey).data(using: .utf8)!.base64EncodedString())"
request.allHTTPHeaderFields = ["Authorization": spotifyAuthKey,
"Content-Type": "application/x-www-form-urlencoded"]

var requestBodyComponents = URLComponents()
let scopeAsString = stringScopes.joined(separator: " ")

requestBodyComponents.queryItems = [
URLQueryItem(name: "client_id", value: spotifyClientId),
URLQueryItem(name: "grant_type", value: "authorization_code"),
URLQueryItem(name: "code", value: responseCode!),
URLQueryItem(name: "redirect_uri", value: redirectUri.absoluteString),
URLQueryItem(name: "code_verifier", value: ""), // not currently used
URLQueryItem(name: "scope", value: scopeAsString),
]

request.httpBody = requestBodyComponents.query?.data(using: .utf8)

let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, // is there data
let response = response as? HTTPURLResponse, // is there HTTP response
(200 ..< 300) ~= response.statusCode, // is statusCode 2XX
error == nil else { // was there no error, otherwise ...
print("Error fetching token \(error?.localizedDescription ?? "")")
return completion(nil, error)
}
let responseObject = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
print("Access Token Dictionary=", responseObject ?? "")
completion(responseObject, nil)
}
task.resume()
}

func fetchArtwork(for track: SPTAppRemoteTrack) {
appRemote.imageAPI?.fetchImage(forItem: track, with: CGSize.zero, callback: { [weak self] (image, error) in
if let error = error {
print("Error fetching track image: " + error.localizedDescription)
} else if let image = image as? UIImage {
self?.imageView.image = image
}
})
}

func fetchPlayerState() {
appRemote.playerAPI?.getPlayerState({ [weak self] (playerState, error) in
if let error = error {
print("Error getting player state:" + error.localizedDescription)
} else if let playerState = playerState as? SPTAppRemotePlayerState {
self?.update(playerState: playerState)
}
})
}
}

Congratulations !

We have completed our application, now you can use other functions of spotify sdk as you wish.

You will have to try the app on your phone as the app needs permission from spotify. It will not work in the simulator. You can browse here to use the application from your phone.

I’m adding the source I got help here : Swift Arcade Youtube

--

--