既存プロジェクトのAPIリクエスト周りの処理を見直した際に出くわした問題と、その対応のために行った小細工のメモ。
もともとやりたかったこと
- APIリクエストのデコード処理にCodable(Decodable)を利用する
- リクエストにDecodableに適合したオブジェクトを紐付け、リクエスト実行とデコードを共通的に行えるようにする
- 簡易的なAPIKitのようなイメージ
実装における(緩い)制約
APIリクエスト処理にはもともとAlamofireを使用していたので、従来のコードを生かすため、Alamofireベースで処理を行う(=APIKitへの乗り換えはしない)
やったこと(基本)
ここでは特に変わったことはしていない。204 No Content
時の対応についてはこちら
1. 抽象リクエストの作成
リクエストの情報(=Alamofire.request()
の引数とするもの)とレスポンスの型を紐付けるための型を定義。
レスポンスの型はデコード処理を簡潔に記述できるよう、Decodable
に適合するよう制約を付加。
import Alamofire
struct AbstractRequest<R: Decodable> {
typealias Response = R
var urlString: URLConvertible
var method: Alamofire.HTTPMethod
var parameters: [String: Any]?
var encoding: ParameterEncoding
var headers: [String: String]
}
※ ↑実際のコードとは一部異なるがだいたいのイメージとして(以降のコードも同様)
2. 抽象リクエストを引数としたリクエスト処理の実装
上述のAbstractRequest
を引数としてリクエストの作成/レスポンスのデコードを行えるようにApiClientの処理を定義。
処理内容はAlamofireでリクエストを作成・実行し、JSONDecoder
でデコードする程度。
import Alamofire
class ApiClient {
func send<R>(_ abstract: AbstractRequest<R>,
completion: @escaping (Result<R, Error>) -> Void) {
let request = Alamofire.request(
abstract.urlString,
method: abstract.method,
parameters: abstract.parameters,
encoding: abstract.encoding,
headers: abstract.headers)
request.validate(statusCode: 200..<400).response { response in
// エラーハンドリングなど
do {
let r = try JSONDecoder().decode(R.self, from: response.data!)
completion(.success(r))
} catch let error {
completion(.failure(error))
}
}
}
}
↑の実装で困ったこと
ここまでの実装で基本的にはAPIリクエストの実行、レスポンスのデコードが行える。
しかし、レスポンスのステータスが204 No Content
であった場合、デコード対象のデータが存在せずエラーが発生する。
やったこと(204 No Content
対応)
1. 専用オブジェクトの定義
204 No Content
のレスポンスが返るケース専用のオブジェクトを定義。
/// No Contentが返るケース専用のためプロパティは持たない
/// AbstractRequestに紐付けられるよう、Decodableに適合させておく
struct NoContent: Decodable {}
// NoContentを利用した際のAbstractRequestのイメージ
// let req = AbstractRequest<NoContent>(
// "https://example.com/api/item/123,
// method: .delete,
// …
// )
2. APIClientを修正
デコード処理の前にレスポンスステータスの判定を追加。
ステータスが204の場合、NoContent
を返すようにする。
request.validate(statusCode: 200..<400).response { response in
// エラーハンドリングなど
+
+ if response.response!.statusCode == 204,
+ let noContent = NoContent() as? R {
+ completion(.success(noContent))
+ return
+ }
do {
let r = try JSONDecoder().decode(R.self, from: response.data!)
completion(.success(r))
}
なぜこうしたか
値が存在しないケースも扱えるようにするのであれば、APIClient.send()
のcompletion handlerの引数をResult<R?, String>
にする選択肢もあった。
しかしこの場合、すべてのAPIリクエストにおいて(大半は不要なはずの)アンラップ処理が各所に蔓延する問題が生じる。
この問題を避け、APIClient内のみで解決できるよう、上記の方法とした。
(NoContent.swift
なる虚無感あふれるファイルが誕生していたり、無理やり感は否めないので、もっとうまいやり方があればコメントで指摘いただけると嬉しいです)