Swift Alamofire + Codableでapiリクエストでの最低限必要そうな処理まとめ

はじめに

アプリ開発でWebAPIにリクエストしてレスポンスを画面に表示したりデータを更新したりありがちな処理をまとめておく。

やりたいことは主に以下
APIへリクエスト(Get、Post、Patch、Delete)
APIのレスポンスからCodableなクラスのインスタンスを作成
・レスポンスヘッダー(認証情報等)を取得
・リクエストヘッダー(認証情報等)を設定してリクエス
・ファイルのアップロード
・エラー処理
・まとめ

APIへリクエスト(Get、Post、Patch、Delete)

とりあえずAPIへリクエス

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?
}

apiリクエス

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