Skip to content

Instantly share code, notes, and snippets.

@Saturn-V
Created March 1, 2024 23:12
Show Gist options
  • Save Saturn-V/d5b0bc1dae833077703343a2c5077e67 to your computer and use it in GitHub Desktop.
Save Saturn-V/d5b0bc1dae833077703343a2c5077e67 to your computer and use it in GitHub Desktop.
Persistent Auth using the Spotify iOS SDK [attempt]
import UIKit
import SwiftUI
@available(iOS 14.0, *)
class ViewController: UIViewController, SPTSessionManagerDelegate, SPTAppRemoteDelegate, SPTAppRemotePlayerStateDelegate {
@AppStorage("clientSession") var clientSession = Data()
private let SpotifyClientID = "[REDACTED]"
private let SpotifyRedirectURI = URL(string: "spotify-login-sdk-test-app://spotify-login-callback")!
lazy var configuration: SPTConfiguration = {
let configuration = SPTConfiguration(clientID: SpotifyClientID, redirectURL: SpotifyRedirectURI)
// Set the playURI to a non-nil value so that Spotify plays music after authenticating and App Remote can connect
// 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:"https://api.com/swap")!
configuration.tokenRefreshURL = URL(string:"https://api.com/refresh")!
return configuration
}()
lazy var sessionManager: SPTSessionManager = {
let manager = SPTSessionManager(configuration: configuration, delegate: self)
print("sessionManager.init: checking for session in AppStorage")
if !self.clientSession.isEmpty {
print("sessionManager.init: found session in AppStorage, unarchiving data")
do {
let object = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(self.clientSession)
manager.session = object as? SPTSession
} catch {
print("sessionManager.init: error unarchiving data")
}
}
return manager
}()
lazy var appRemote: SPTAppRemote = {
let appRemote = SPTAppRemote(configuration: self.configuration, logLevel: .debug)
appRemote.delegate = self
appRemote.connectionParameters.accessToken = self.sessionManager.session?.accessToken
return appRemote
}()
private var lastPlayerState: SPTAppRemotePlayerState?
// MARK: - Subviews
private lazy var connectLabel: UILabel = {
let label = UILabel()
label.text = "Connect your Spotify account"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private lazy var connectButton = ConnectButton(title: "CONNECT")
private lazy var disconnectButton = ConnectButton(title: "DISCONNECT")
private lazy var pauseAndPlayButton: UIButton = {
let button = UIButton()
button.addTarget(self, action: #selector(didTapPauseOrPlay), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
return imageView
}()
private lazy var trackLabel: UILabel = {
let trackLabel = UILabel()
trackLabel.translatesAutoresizingMaskIntoConstraints = false
trackLabel.textAlignment = .center
return trackLabel
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
if !self.clientSession.isEmpty {
self.didTapConnect()
}
view.addSubview(connectLabel)
view.addSubview(connectButton)
view.addSubview(disconnectButton)
view.addSubview(imageView)
view.addSubview(trackLabel)
view.addSubview(pauseAndPlayButton)
let constant: CGFloat = 16.0
connectButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
connectButton.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
disconnectButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
disconnectButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -50).isActive = true
connectLabel.centerXAnchor.constraint(equalTo: connectButton.centerXAnchor).isActive = true
connectLabel.bottomAnchor.constraint(equalTo: connectButton.topAnchor, constant: -constant).isActive = true
imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
imageView.topAnchor.constraint(equalTo: view.topAnchor, constant: 64).isActive = true
imageView.bottomAnchor.constraint(equalTo: trackLabel.topAnchor, constant: -constant).isActive = true
trackLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
trackLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: constant).isActive = true
trackLabel.bottomAnchor.constraint(equalTo: connectLabel.topAnchor, constant: -constant).isActive = true
pauseAndPlayButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
pauseAndPlayButton.topAnchor.constraint(equalTo: trackLabel.bottomAnchor, constant: constant).isActive = true
pauseAndPlayButton.widthAnchor.constraint(equalToConstant: 50).isActive = true
pauseAndPlayButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
pauseAndPlayButton.sizeToFit()
connectButton.sizeToFit()
disconnectButton.sizeToFit()
connectButton.addTarget(self, action: #selector(didTapConnect(_:)), for: .touchUpInside)
disconnectButton.addTarget(self, action: #selector(didTapDisconnect(_:)), for: .touchUpInside)
updateViewBasedOnConnected()
}
func update(playerState: SPTAppRemotePlayerState) {
if lastPlayerState?.track.uri != playerState.track.uri {
fetchArtwork(for: playerState.track)
}
lastPlayerState = playerState
trackLabel.text = playerState.track.name
if playerState.isPaused {
pauseAndPlayButton.setImage(UIImage(named: "play"), for: .normal)
} else {
pauseAndPlayButton.setImage(UIImage(named: "pause"), for: .normal)
}
}
func updateViewBasedOnConnected() {
if (appRemote.isConnected) {
connectButton.isHidden = true
disconnectButton.isHidden = false
connectLabel.isHidden = true
imageView.isHidden = false
trackLabel.isHidden = false
pauseAndPlayButton.isHidden = false
} else {
disconnectButton.isHidden = true
connectButton.isHidden = false
connectLabel.isHidden = false
imageView.isHidden = true
trackLabel.isHidden = true
pauseAndPlayButton.isHidden = true
}
}
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)
}
})
}
// MARK: - Actions
@objc func didTapPauseOrPlay(_ button: UIButton) {
if let lastPlayerState = lastPlayerState, lastPlayerState.isPaused {
appRemote.playerAPI?.resume(nil)
} else {
appRemote.playerAPI?.pause(nil)
}
}
@objc func didTapDisconnect(_ button: UIButton) {
if (appRemote.isConnected) {
appRemote.disconnect()
}
}
@objc func didTapConnect(_ button: UIButton) {
/*
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 scope: SPTScope = [.appRemoteControl, .playlistReadPrivate]
if #available(iOS 11, *) {
// Use this on iOS 11 and above to take advantage of SFAuthenticationSession
sessionManager.initiateSession(with: scope, options: .clientOnly)
} else {
// Use this on iOS versions < 11 to use SFSafariViewController
sessionManager.initiateSession(with: scope, options: .clientOnly, presenting: self)
}
}
func didTapConnect() {
/*
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 scope: SPTScope = [.appRemoteControl, .playlistReadPrivate]
if #available(iOS 11, *) {
// Use this on iOS 11 and above to take advantage of SFAuthenticationSession
sessionManager.initiateSession(with: scope, options: .clientOnly)
} else {
// Use this on iOS versions < 11 to use SFSafariViewController
sessionManager.initiateSession(with: scope, options: .clientOnly, presenting: self)
}
}
// MARK: - SPTSessionManagerDelegate
func sessionManager(manager: SPTSessionManager, didFailWith error: Error) {
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")
do {
print("sessionManager.didRenew: archiving and storing session in AppStorage")
let sessionData: Data = try NSKeyedArchiver.archivedData(
withRootObject: session,
requiringSecureCoding: false
)
self.clientSession = sessionData
} catch {
print("sessionManager.didRenew: error archiving and storing session")
}
}
func sessionManager(manager: SPTSessionManager, didInitiate session: SPTSession) {
appRemote.connectionParameters.accessToken = session.accessToken
appRemote.connect()
do {
print("sessionManager.didInit: archiving and storing session in AppStorage")
let sessionData: Data = try NSKeyedArchiver.archivedData(
withRootObject: session,
requiringSecureCoding: false
)
self.clientSession = sessionData
} catch {
print("sessionManager.didInit: error archiving and storing session")
}
}
// MARK: - 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
func playerStateDidChange(_ playerState: SPTAppRemotePlayerState) {
update(playerState: playerState)
}
// 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)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment