LoginSignup
0
1

More than 1 year has passed since last update.

[swift]SpotifyAPIを叩いてプレイリストを取得する(OAuth2.0)

Last updated at Posted at 2022-02-22

API通信の学習をする上で、SpotifyのAPIを使用してみたいと思いサンプルを作成しました。

自身の備忘録としても残しておこうと思います。

サンプル概要

・OAuth2.0認証を利用し、自身のSpotifyデータと連携
→プレイリストを取得、TableViewにプレイリスト名を表示
→CellをタップするとSpotifyのプレイリストページが開く

・アーキテクチャはMVC

サンプル完成後

ビルドするとこんな感じです。
ezgif-2-48e5958d2c.gif

事前に行うこと

・Spotifyのユーザー登録
・SpotifyDevelopersに登録(下記で詳しく)

SpotifyDevelopers登録方法

Developers登録をする

下記サイトから登録をする
https://developer.spotify.com/

メニューからDashBoardを選択,登録画面に進み
登録後clientIDとclientSecretが取得できるので控えておく。

リダイレクトURL設定

・xcodeでの設定

Target->Info->URLTypesで設定
スクリーンショット 2022-02-22 20.59.45.png

・SpotifyDevelopers側での設定

管理画面→EDIT SETTINGS→Redirect URIsを入力する
スクリーンショット-2022-02-22-21.05.38.jpg

※xcodeで設定したものを入力

xcode内でspotifysampleappとしているので
://callbackをつけて入力

ソースコード

ではサンプルのコードをみていきます。

自前Errorの用意

レスポンスをResult型で受け取るときには、Error型から返すことになるためちゃんと用意しておきます。

API.swift
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やパラメータのハードコーディングを減らすため、あらかじめ定数化しておきます。

API.swift
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/

認証リクエスト(アクセストークン取得)

LoginViewController.swift

 @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に立ち上げ後の処理を記述します。

AppDelegate.swift
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    authViewController.openURL(url: url)
}

認証するViewControllerでレスポンスを処理します。

LoginViewController.swift

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を使用)

API.swift
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を作成しておきます。

SpotifyAccessTokenModel.swift
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のメソッドを呼び出して,プレイリストを取得します。
ヘッダーにアクセストークンを付けてリクエストを送ります。

API.swift
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を作成しておきます。

PlayListModel.swift
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に表示

ListViewController
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で先ほどのプレイリスト取得メソッドを呼びます。

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

間違っているところもあると思いますが、日々精進していきたいと思います。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1