はじめに
iOS開発でAPI通信の学習をしています。
せっかくなので皆がよく知るAPIを使いたいと思い、SpotifyAPIを利用することにしました。
OAuth2.0認証を利用し、自身のSpotifyデータと連携する必要があるのですが、
その過程で苦戦した所や、他でも活用できそうな所を備忘録的に残しておこうと思います。
また、これからAPIの学習をする方の参考になれば幸いです。
環境
私が実行した時の環境
- Xcode 12.5.1
- iOS Deployment Target 13.0
- Alamofire5.4.3 (Swift Package Manager)
アーキテクチャはMVCを想定
※エラーハンドリング等の記述はだいぶ省略していたりするので、あまり参考にしないほうがいいです
用意しておくもの
- Spotifyのユーザー登録
SpotifyDevelopersに登録
メニューからDashBoardを選択し登録画面に進む。
登録するとclientID
とclientSecret
が取得できるので控えておく。
リダイレクトURIの設定
アプリ側の設定
info plistからも見れる
(私はだいたいアプリのBundleIdentifireをベースにしていますが、Spotifyのガイドに制約が記載されているのでそれに準拠するようにします)
※SpotifyDeveloper App Settingsより抜粋ホスト側の設定(SpotifyDevelopers)
管理画面からEDIT SETTINGS
に行き、Redirect URIs
を入力する
※BundleIDSも入力例に沿った形で入力する
ここで、アプリ側で設定しておいたものを入力。
アプリでmynamesampleapp
としているので、Redirect URIsにmynamesampleapp://callback
と入力し登録。
※callbackの部分は無くても良いが、callbackとかredirectにするのが一般的な感じ。
Errorの用意
API通信周りのエラー
レスポンスを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 = "clientID" // 自分のclientIDを入れる
let clientSecret = "clientSecret" // 自分の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 = "設定済みのリダイレクトURI" // 設定済みのredirectURIを入れる
let stateStr = "stateを入力"
let grantType = "authorization_code"
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")!
}
// ~~~ 以下に通信メソッドを記述する ~~~
}
scopesは必要なものを入れる。
spotifyAPIでは%20
を入れて繋ぐ。
認証リクエスト(アクセストークンの取得)
認証用のボタンがあったと仮定して、ボタンタップで認証リクエストを送る
@IBOutlet weak var authButton: UIButton! {
didSet {
loginButton.addTarget(self, action: #selector(tapAuthButton), for: .touchUpInside)
}
}
@objc func tapAuthButton() {
// APIクラスから認証用のURLを呼び出して開く
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)
}
認証を担当するVCでレスポンスURLの処理をする
URLに含まれるcode
がアクセストークンを取得するために必要な認証コードとなる(response_typeをcodeに指定した場合)
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 else {
return
}
// キーを保持
UserDefaults.standard.spotifyAccessToken = _accessToken.token
}
}
POSTリクエストの中身。Alamofireを使用している。
func postAuthorizationCode(code: String, completion: ((SpotifyAccessTokenModel?, Error?) -> Void)? = nil) {
guard let url = URL(string: getTokenEndPoint) else {
completion?(nil, APIError.postAuthorizationCode)
return
}
let basicAuthCode = Contstants.clientID+":"+Contstants.clientSecret
let data = basicAuthCode.data(using: .utf8)
guard let base64AuthCode = data?.base64EncodedString() else {
completion?(nil, APIError.postAuthorizationCode)
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)
}
}
}
ヘッダーにclientIDとclientSecretを入れる際、base64に変換しておく必要がある。
ユーザーデータの取得
コントローラーからAPIのメソッドを呼び出して、データを取得する。
ヘッダーにアクセストークンを付けてリクエストを送る。
リクエストにはAlamofireを使用している。
func getCurrentUserProfile(completion: @escaping (Result<UserProfile, Error>) -> Void) {
guard UserDefaults.standard.spotifyAccessToken != "" else { return }
guard let url = URL(string: Contstants.baseAPIURL + "/me") else {
completion(.failure(APIError.failedToGetData))
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 result = try JSONDecoder().decode(UserProfile.self, from: _data)
print(result)
completion(.success(result))
} catch let error {
print(error.localizedDescription)
completion(.failure(error))
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
fetchUserProfile()
}
private func fetchUserProfile() {
API.shared.getCurrentUserProfile { [weak self] result in
switch result {
case .success(let model):
// userを保持する例
self?.currentUser = model
break
case .failure(let error):
print(error.localizedDescription)
}
}
}
その他
ユーザーデータとほぼ同じ手順で、プレイリストやその他のデータも取得できます。
新たな学びがあったら追記していきます。
おわりに
Spotifyはドキュメントが親切で、しっかりと読んでいけばちゃんと答えに辿り着けるようになっていました。
(英語なので翻訳しながら読む必要がありますが汗)
コードについては、試行錯誤しながらあれこれやっていたのであまり綺麗ではないかもしれません。
この辺はコードレビュー受けるなどしてブラッシュアップしていきたいです。