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型への対応とパラメータ名がスネークケースの場合にもキャメルケースに対応することができる。というのを上にも入れてみた。ダメだ。。力尽きて相変わらず適当になった。。以上です。