Swift Alamofire + Codableでapiリクエストでの最低限必要そうな処理まとめ
はじめに
アプリ開発でWebAPIにリクエストしてレスポンスを画面に表示したりデータを更新したりありがちな処理をまとめておく。
やりたいことは主に以下
・APIへリクエスト(Get、Post、Patch、Delete)
・APIのレスポンスからCodableなクラスのインスタンスを作成
・レスポンスヘッダー(認証情報等)を取得
・リクエストヘッダー(認証情報等)を設定してリクエスト
・ファイルのアップロード
・エラー処理
・まとめ
APIへリクエスト(Get、Post、Patch、Delete)
let url = "http://localhost:3000/path/to" let method = HTTPMethod.get // .post or .patch or .delete let parameters:[String:Any] = [ "param": "xxxxx", "param2": xxxxx ] Alamofire.request(url, method: method, parameters: parameters) .responseJSON { response in response.result.value /* Optional({ user = { id = 1, name = "Tom" comment = nil icon = { main = "http://xxxxx" sub = nil } } }) */ }
APIのレスポンスからCodableなクラスのインスタンスを作成
上のような形で取得した値をそのままディクショナリとして使うと中身を都度キャストしたり扱いずらい。そこで予めCodableなクラスを定義しておくと簡単に扱える。
Model.swift
class User: Codable { let id: Int let name: String let comment: String? // 任意な項目 let icon: Icon // 別のCodableなクラス } class Icon: Codable { let main: String? let sub: String? }
Alamofire.request(url, method: method, parameters: parameters) .response { response in guard let data = response.data else { return } let decoder = JSONDecoder() let user = try? decoder.decode(User.self, from: data) /* Optional({ id: 1 name: "Tom" comment: nil icon: { main: "http://xxxxx" sub: nil } }) */ /* 簡単に扱える!! user.id、user.name、user.icon.main */ } }
レスポンスヘッダー(認証情報等)を取得
ログインAPIへリクエストして成功したら認証情報がレスポンスヘッダーにセットされて返ってくるので以降のリクエストはリクエストヘッダーに認証情報をセットしてリクエストする。みたいなのはよくある実装だと思うのでそれを$$
class Auth: Codable { token: String } Alamofire.request(url, method: method, parameters: parameters) .response { response in let decoder = JSONDecoder() let auth = try? decoder.decode(type, from: JSON(response.response?.allHeaderFields as Any).rawData()) /* Optional({ token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }) */ }
リクエストヘッダー(認証情報等)を設定してリクエスト
取得した認証情報をリクエストヘッダーに設定してリクエスト。といってもheadersにセットするだけ。
let headers = [ "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ] Alamofire.request(url, method: method, parameters: parameters, headers: headers) .response { response in }
ファイルのアップロード
ファイルアップロードは今までのrequestメソッドではなくuploadメソッドを使う。
let fileUrl = URL("file://path/to") // ファイルのパス let fileKey = "upload_file" // パラメータ名 Alamofire.upload(multipartFormData:{ multipartFormData in // アップロードファイルをセット multipartFormData.append(fileUrl, withName: fileKey) // その他のパラメータをセット(Dataに変換して1つずつセットしていく) parameters?.keys.forEach({ (key) in if let data = convertData(value: parameters![key]!) { multipartFormData.append(data, withName: key) } }) }, usingThreshold:UInt64.init(), to: url, method: .post, headers: headers encodingCompletion: { result in // }) func convertData(value: AnyObject?) -> Data? { if let string = value as? String { return string.data(using: .utf8) } else if let int = value as? Int { return String(int).data(using: .utf8) } else if let bool = value as? Bool { return String(bool ? 1 : 0).data(using: .utf8) } else { return nil } }
エラー処理
httpステータスコードが200番台じゃなかったらエラーと判定。validateメソッドで条件を指定する
Alamofire.request(url, method: method, parameters: parameters) .validate(statusCode: 200..<300) .response { response in if response.error != nil { // エラー } }
まとめ
API共通クラス
import Foundation import Alamofire import SwiftyJSON import UIKit class ApiManager { // ログイン static func login<T : Codable>(url:String, type:T.Type, params:[String: Any]? = nil, success:((T?)->())? = nil, fail:((Error?)->())? = nil) { Alamofire.request(url, method: .post, parameters: params) .validate(statusCode: 200..<300) .response { response in if response.error != nil { fail?(response.error) } else { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase success?(try? decoder.decode(type, from: JSON(response.response?.allHeaderFields as Any).rawData())) } } } // ファイルUPLOAD static func upload<T : Codable>(token: String, url:String, type:T.Type, fileKey: String, fileUrl: URL, params:[String: AnyObject?]? = nil, success:((T?)->())? = nil, fail:((Error?)->())? = nil) { Alamofire.upload(multipartFormData:{ multipartFormData in multipartFormData.append(fileUrl, withName: fileKey) params?.keys.forEach({ (key) in if let data = ApiManger.convertData(value: params![key]!) { multipartFormData.append(data, withName: key) } }) }, usingThreshold:UInt64.init(), to: url, method: .post, headers: [ "token": token ], encodingCompletion: { result in switch result { case .success(let upload, _, _): upload.validate(statusCode: 200..<300).responseData(completionHandler: { response in if response.error != nil { fail?(response.error) } else { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase success?(try? decoder.decode(type, from: response.data!)) } }) case .failure(let error): fail?(error) } }) } static func convertData(value: AnyObject?) -> Data? { if let string = value as? String { return string.data(using: .utf8) } else if let int = value as? Int { return String(int).data(using: .utf8) } else if let bool = value as? Bool { return String(bool ? 1 : 0).data(using: .utf8) } else { return nil } } static func request<T : Codable>(token: String, url:String, method:HTTPMethod, type:T.Type, params:[String: Any]? = nil, success:((T?)->())? = nil, fail:((Error?)->())? = nil) { Alamofire.request(url, method: method, parameters: params, headers: [ "token": token ]) .validate(statusCode: 200..<300) .response { response in if response.error == nil { let decoder = JSONDecoder() let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" decoder.dateDecodingStrategy = .formatted(formatter) decoder.keyDecodingStrategy = .convertFromSnakeCase success?(try? decoder.decode(type, from: response.data!)) } else if let code = response.response?.statusCode { fail?(response.error) } }
// 呼び出す
let url = "http://xxxx/path/to let token = "xxxxxxxxxxxxxxxxxxxxxxxxxx" let params = [ "hoge": "fuga" ] ApiProvider.patch(token: token, url: url, type: User.self, params: params ,success: { user in // success }, fail: { error in // error or fail })
ずらずら書いたけどこんな感じで共通化しておく。Codableの機能でDate型への対応とパラメータ名がスネークケースの場合にもキャメルケースに対応することができる。というのを上にも入れてみた。ダメだ。。力尽きて相変わらず適当になった。。以上です。