2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

AlamofireのURLRequestConvertibleをAPIKitのRequest型風に使いたい

Posted at

最初に結論

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/gethttps://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

それをコードで表現すると次のようになります

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で名前空間みたいにすればいいのでは?
2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?