Created
March 1, 2024 23:12
-
-
Save Saturn-V/d5b0bc1dae833077703343a2c5077e67 to your computer and use it in GitHub Desktop.
Persistent Auth using the Spotify iOS SDK [attempt]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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