最初に結論
AlamofireのURLRequestConvertible
は公式リファレンスを見るとenum Router
を使ってリクエスト方法を列挙してるんですな。
enum Router: URLRequestConvertible {
case get, post
var baseURL: URL {
return URL(string: "https://httpbin.org")!
}
var method: HTTPMethod {
switch self {
case .get: return .get
case .post: return .post
}
}
var path: String {
switch self {
case .get: return "get"
case .post: return "post"
}
}
func asURLRequest() throws -> URLRequest {
let url = baseURL.appendingPathComponent(path)
var request = URLRequest(url: url)
request.method = method
return request
}
}
AF.request(Router.get)
でも、こういうふうに列挙してもそれを網羅する意味はないと私は思います。この例のhttps://httpbin.org/get
とhttps://httpbin.org/post
を網羅する意味ないと思うんですよね。それはパターンの漏れを考慮する必要性を感じないからです。
ただ、列挙することで一覧性があるという長所は認めます。しかしWeb APIが増えると見辛くなります。そうなると分割すればいいと思うのは当然なんですが、唯一の長所である一覧性は落ちるので増えたら長所がないんです。
つまり、enumでやる意味がないことをenumでやってしまっているので他の方法でやりゃーいいじゃんというのが主張です。
そのためにAlamofire v5くらいからのresponseDecodable
を使って、APIKitのように型でリクエストを表現したいということを書いておきます。
- URLRequestConvertibleは便利だけどそれをenumに準拠させる必要はない
- responseDecodableメソッドもある
前提知識
Decodable
Alamofire v5くらいからResponseをJSONでデコードした型に変換してくれるメソッドがあります
// こいつが通信成功後の型
struct HTTPBinResponse: Decodable {
let url: String
}
// 通信成功したらHTTPBinResponseに変換する
AF.request("https://httpbin.org/get").responseDecodable(of: HTTPBinResponse.self) { response in
debugPrint("Response: \(response)")
}
これでresponseData
メソッドからData型から変換したり、responseJSON
メソッドでDictionaryから変換する必要はなくなってきます。もちろんSwiftのDecodableに準拠させてたら変換自体はやらんけども。
本題
リクエスト用のprotocolを考える
APIKitのようにリクエスト型をリクエストごとに用意したいので、リクエスト型が準拠したいprotocolを考えます。
protocol WebAPIRequest: Alamofire.URLRequestConvertible {
// 認証が不要な場合はnil
var accessToken: String? { get }
// 接続先を各Requestごとに変えられていい
var baseURL: URL { get }
var path: String { get }
var method: Alamofire.HTTPMethod { get }
var parameters: Alamofire.Parameters { get }
// アプリのバージョンとかをHeaderに入れるための
func addInformationToURLRequest(_ urlRequest: Foundation.URLRequest) -> Foundation.URLRequest
}
よくある利用例として、ヘッダーに自動で情報を入れてもらうための処理はリクエストごとに勝手に組み立てて欲しいので、addInformationToURLRequest
メソッドをprotocol extensionで自動的にやっておいてもらう
extension WebAPIRequest {
func addInformationToURLRequest(_ urlRequest: Foundation.URLRequest) -> Foundation.URLRequest {
var request = urlRequest
// これはサーバー側が欲しい情報を組み立てたりする
let headers = [
/* 省略 */
]
headers.forEach { key, value in
request.addValue(value, forHTTPHeaderField: key)
}
return request
}
}
Request型に準拠したリクエストを作る
リクエストを型で表現したいのでやりたいことは次のようなことです
-
https://httpbin.org/get
にアクセスする例- baseURLが
https://httpbin.org
- pathが
/get
- HTTPMethodが
get
- accessTokenはnil
- レスポンスは
HTTPBinResponse
型
- baseURLが
それをコードで表現すると次のようになります
struct HTTPBinResponse: Decodable {
let url: String
}
struct GetRequest: WebAPIRequest {
typealias Response = HTTPBinResponse
let path = "/get"
let method = HTTPMethod.get
let baseURL: URL
// 今回は使わない
let accessToken: String?
// 今回は使わない
let parameters = Parameters()
func asURLRequest() throws -> URLRequest {
var urlRequest = URLRequest(url: baseURL.appendingPathComponent(path))
urlRequest.httpMethod = method.rawValue
urlRequest = addDeviceInformationToURLRequest(urlRequest)
return try Alamofire.URLEncoding.queryString.encode(urlRequest, with: parameters)
}
}
利用時は次のような感じです。
let request = GetRequest(baseURL: baseURL)
AF.request(request)
.validate()
.responseDecodable(of: GetRequest.Response.self) {
debugPrint("Response: \($0)")
}
responseDecodable
に戻り値の型を自前で指定しているところはリクエストが決まってるなら自動的に決まるはずで、工夫すればもう少し良くなりそうですが、まあそれは込み入ってくるので各自良さそうなやり方をコメントに書いてもらうのが良いでしょう。
一覧性はないが実装済みのリクエストを把握するための方法
Alamofire公式のenum Routerのような一覧性がほしいという要望もあるとは思いますが、caseなしのenumを使うことで、実装済みのリクエストを把握するための方法で事足りる気がします。
具体的にはenumの下に置いて実装するようにします。
enum WebAPI { }
extension WebAPI {
struct GetRequest: WebAPIRequest {
/* 省略 */
}
}
extension WebAPI {
struct PostRequest: WebAPIRequest {
/* 省略 */
}
}
ちなみに全てのリクエストを1ファイルに書いてしまうとめちゃ長くなるのでファイルを分けたほうが絶対良いでしょうね。
まとめ
- Alamofireの公式ではenum Routerを作ってるが筆者は意味がない気がしてる
- caseで列挙するのはいいがcaseで網羅しなきゃいけない必要性を筆者は感じない
- enum caseで網羅するんじゃなく、structで型としてそれぞれのAPI仕様を示せば?
- enum Routerの一覧性のメリット
- caseなしenumで名前空間みたいにすればいいのでは?