API通信の学習をする上で、SpotifyのAPIを使用してみたいと思いサンプルを作成しました。
自身の備忘録としても残しておこうと思います。
##サンプル概要
・OAuth2.0認証を利用し、自身のSpotifyデータと連携
→プレイリストを取得、TableViewにプレイリスト名を表示
→CellをタップするとSpotifyのプレイリストページが開く
・アーキテクチャはMVC
##事前に行うこと
・Spotifyのユーザー登録
・SpotifyDevelopersに登録(下記で詳しく)
##SpotifyDevelopers登録方法
###Developers登録をする
下記サイトから登録をする
https://developer.spotify.com/
メニューからDashBoardを選択,登録画面に進み
登録後clientIDとclientSecretが取得できるので控えておく。
###リダイレクトURL設定
####・xcodeでの設定
Target->Info->URLTypesで設定
####・SpotifyDevelopers側での設定
管理画面→EDIT SETTINGS→Redirect URIsを入力する
※xcodeで設定したものを入力
xcode内でspotifysampleappとしているので
://callbackをつけて入力
##ソースコード
ではサンプルのコードをみていきます。
###自前Errorの用意
レスポンスをResult型で受け取るときには、Error型から返すことになるためちゃんと用意しておきます。
enum APIError: Error, LocalizedError {
case authorizationCodeError, urlError, taskError
var errorDescription: String? {
switch self {
case .authorizationCodeError:
return "認証エラーが発生しています"
case .urlError:
return "URLが取得出来ませんでした"
case .taskError:
return "データの取得が出来ませんでした"
}
}
}
###APIクラス
URLやパラメータのハードコーディングを減らすため、あらかじめ定数化しておきます。
final class API {
// シングルトン
static let shared = API()
private init() {}
let clientID = "f7515669c76b4ba39f69d0acb585949c"
# // 自分のclientIDを入れる
let clientSecret = "c47349123ad447639b6d9ff4f9218e37"
#// 自分のclientSecretを入れる
let baseOAuthURL = "https://accounts.spotify.com/authorize"
let baseAPIURL = "https://api.spotify.com/v1"
let getTokenEndPoint = "https://accounts.spotify.com/api/token"
let scopes = "user-read-private%20playlist-read-private%20playlist-read-collaborative"
let redirectURI = "spotifysampleapp://callback"
#// 設定済みのredirectURIを入れる
let stateStr = "bb17785d811bb1913ef54b0a7657de780defaa2d"
let grantType = "authorization_code"
static let jsonDecoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
return decoder
}()
enum URLParameterName: String {
case clientID = "client_id"
case clientSecret = "client_secret"
case redirectURI = "redirect_uri"
case grantType = "grant_type"
}
var oAuthURL: URL {
return URL(string: "\(baseOAuthURL)?response_type=code&client_id=\(clientID)&scope=\(scopes)&redirect_uri=\(redirectURI)&state=\(stateStr)&show_dialog=TRUE")!
}
func postAuthorizationCode(code: String, completion: ((SpotifyAccessTokenModel?, Error?) -> Void)? = nil) {
guard let url = URL(string: getTokenEndPoint) else {
completion?(nil, APIError.authorizationCodeError)
return
}
let basicAuthCode = clientID+":"+clientSecret
let data = basicAuthCode.data(using: .utf8)
guard let base64AuthCode = data?.base64EncodedString() else {
completion?(nil, APIError.authorizationCodeError)
return
}
let parameters = [
"grant_type": "authorization_code",
"code": code,
"redirect_uri": redirectURI
]
let getTokenHeaders: HTTPHeaders = [
"Authorization": "Basic \(base64AuthCode)"
]
AF.request(url, method: .post, parameters: parameters, headers: getTokenHeaders).responseJSON { (response) in
do {
guard let _data = response.data else {
completion?(nil, APIError.taskError)
return
}
let accessToken = try JSONDecoder().decode(SpotifyAccessTokenModel.self, from: _data)
completion?(accessToken, nil)
} catch let error {
completion?(nil, error)
}
}
}
func getPlayList(completion: @escaping (Result<[item], Error>) -> Void) {
guard UserDefaults.standard.spotifyAccessToken != "" else { return }
guard let url = URL(string: baseAPIURL + "/me/playlists") else {
completion(.failure(APIError.urlError))
return
}
let headers: HTTPHeaders = [
"Authorization": "Bearer \(UserDefaults.standard.spotifyAccessToken)"
]
AF.request(url, method: .get, headers: headers).responseJSON { (response) in
do {
guard let _data = response.data else { return }
let item = try JSONDecoder().decode(PlayListModel.self, from: _data)
let result = item.items
completion(.success(result))
} catch let error {
print(error.localizedDescription)
completion(.failure(error))
}
}
}
}
scopesは必要なものを入れます。(下記参照)
https://developer.spotify.com/documentation/general/guides/authorization/scopes/
###認証リクエスト(アクセストークン取得)
@IBOutlet private weak var authButton: UIButton! {
didSet {
authButton.addTarget(self, action: #selector(tapAuthButton), for: .touchUpInside)
}
}
@objc private func tapAuthButton(){
UIApplication.shared.open(API.shared.oAuthURL,options: [:])
}
ボタンをタップして、認証リクエストを送ります。
Spotifyにログインし、URLに必要なパラメータがしっかりと入っていれば、認証コードが付いたURLをレスポンスとして受け取れるはずです。
外部サイトからアプリを再度立ち上げることになるので、AppDelegateに立ち上げ後の処理を記述します。
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
authViewController.openURL(url: url)
}
認証するViewControllerでレスポンスを処理します。
func openURL(url: URL) {
guard let queryItems = URLComponents(string: url.absoluteString)?.queryItems,
let code = queryItems.first(where: {$0.name == "code"})?.value,
let getState = queryItems.first(where: {$0.name == "state"})?.value,
getState == API.shared.stateStr
else {
return
}
# // `code`を引数として渡して、トークンリクエストをする
API.shared.postAuthorizationCode(code: code) { accessToken, error in
if let _error = error {
print(_error.localizedDescription)
return
}
guard let _accessToken = accessToken,
let vc = UIStoryboard.init(name: "List", bundle: nil).instantiateInitialViewController()
else {
return
}
# // キーを保持
UserDefaults.standard.spotifyAccessToken = _accessToken.access_token
self.navigationController?.pushViewController(vc, animated: true)
}
}
Postリクエストの中身はこちら(Alamofireを使用)
func postAuthorizationCode(code: String, completion: ((SpotifyAccessTokenModel?, Error?) -> Void)? = nil) {
guard let url = URL(string: getTokenEndPoint) else {
completion?(nil, APIError.authorizationCodeError)
return
}
let basicAuthCode = clientID+":"+clientSecret
let data = basicAuthCode.data(using: .utf8)
guard let base64AuthCode = data?.base64EncodedString() else {
completion?(nil, APIError.authorizationCodeError)
return
}
let parameters = [
"grant_type": "authorization_code",
"code": code,
"redirect_uri": redirectURI
]
let getTokenHeaders: HTTPHeaders = [
"Authorization": "Basic \(base64AuthCode)"
]
AF.request(url, method: .post, parameters: parameters, headers: getTokenHeaders).responseJSON { (response) in
do {
guard let _data = response.data else {
completion?(nil, APIError.taskError)
return
}
let accessToken = try JSONDecoder().decode(SpotifyAccessTokenModel.self, from: _data)
completion?(accessToken, nil)
} catch let error {
completion?(nil, error)
}
}
}
###アクセストークンモデル
JSONをデコードして受け取るModelを作成しておきます。
import Foundation
struct SpotifyAccessTokenModel: Codable {
let access_token:String
let token_type:String
let scope:String
let expires_in:Int
let refresh_token:String
}
###プレイリストの取得
APIのメソッドを呼び出して,プレイリストを取得します。
ヘッダーにアクセストークンを付けてリクエストを送ります。
func getPlayList(completion: @escaping (Result<[item], Error>) -> Void) {
guard UserDefaults.standard.spotifyAccessToken != "" else { return }
guard let url = URL(string: baseAPIURL + "/me/playlists") else {
completion(.failure(APIError.urlError))
return
}
let headers: HTTPHeaders = [
"Authorization": "Bearer \(UserDefaults.standard.spotifyAccessToken)"
]
AF.request(url, method: .get, headers: headers).responseJSON { (response) in
do {
guard let _data = response.data else { return }
let item = try JSONDecoder().decode(PlayListModel.self, from: _data)
let result = item.items
completion(.success(result))
} catch let error {
print(error.localizedDescription)
completion(.failure(error))
}
}
}
ここでもJSONをデコードして受け取るModelを作成しておきます。
import Foundation
struct PlayListModel: Decodable {
let items:[item]
}
struct item:Codable {
var href:String
var id:String
var name:String
var external_urls:ExternalUrl
}
struct ExternalUrl:Codable {
var spotify:String
enum CodingKeys: String, CodingKey {
case spotify = "spotify"
}
var url: URL? { URL.init(string: spotify) }
}
※こちらを参照
https://developer.spotify.com/console/get-current-user-playlists/?limit=-1&offset=1
###TableViewに表示
import UIKit
final class ListViewController: UIViewController {
private let cellID = "UITableViewCell"
@IBOutlet private weak var tableView: UITableView! {
didSet {
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellID)
tableView.delegate = self
tableView.dataSource = self
}
}
private var playListModel:[item] = []
override func viewDidLoad() {
super.viewDidLoad()
API.shared.getPlayList { [weak self] result in
switch result {
case .success(let model):
self?.playListModel = model
self?.tableView.reloadData()
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
extension ListViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let url = playListModel[indexPath.row].external_urls.url,
UIApplication.shared.canOpenURL(url) else {
return
}
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
extension ListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return playListModel.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellID) else {
fatalError()
}
let item = playListModel[indexPath.row]
cell.textLabel?.text = item.name
return cell
}
}
遷移先のListViewControllerで先ほどのプレイリスト取得メソッドを呼びます。
override func viewDidLoad() {
super.viewDidLoad()
API.shared.getPlayList { [weak self] result in
switch result {
case .success(let model):
self?.playListModel = model
self?.tableView.reloadData()
case .failure(let error):
print(error.localizedDescription)
}
}
}
あとはTableViewに表示して、Cellをタップした際は取得したURLでSpotifyに飛ばすだけです。
##終わりに
プレイリストの他にも、アルバムやアーティストの取得も可能みたいです。
(かなり応用性が高いAPI?みたいです)
ソースコードはあれこれ試行錯誤しました。
下記にまとめてあります。
https://github.com/taro-ken/Spotify-Sample-App
間違っているところもあると思いますが、日々精進していきたいと思います。