Alamofire で APIClient を書いてみた

APIClient はどういう設計が良いのかあとずっと悩んでいたのですが、なんとなく形になったような気がしたので書いてみます。
Swift は型安全な言語なのですが、 API から取得される値は様々な型を取り得るので、それを汎用的に扱えるようにするためにはどうしたら良いなかあ、型の扱い方が難しいなあと感じました。

初心者なので鵜呑みにはしないでくださいな。

APIClient

サーバ側で 404 等のエラーが 発生した際は ["error": "エラーメッセージ"] というレスポンスボディが返ってくることを想定しています。

Alamofire では、リクエストメソッドを Alamofire.Method で指定する必要があり、APIClient を使う Class を記述したファイル内で、毎回 import Alamofire しなければならなそうで嫌だなあと思ったので、 Enum を使って Alamofire.Method に変換できるようにしました。

サーバ側でエラーが発生した際には、リクエスト自体には成功しているので response.result.isSuccess() が true となります。
そのため、 response.result.isSuccess() が false となるのはリクエストそのものに失敗しているときだと思うのですが、アプリ側では、サーバ側のエラーとリクエストそのものが失敗したときのエラーを同じエラーとして扱いたいなあと思ったので、 APIResponse という Struct を用意して、 response の値を APIResponse に変換するようにしてみました。

import Alamofire

class APIClient {
    static let host = "http://localhost:3000/"

    static func httpRequest(method: RequestMethod, endpoint: String, parameters: [String: AnyObject]?, handler: (APIResponse) -> Void) {
        Alamofire.request(method.toAlamofile(), host + endpoint, parameters: parameters)
            .responseJSON { response in
                if response.result.isSuccess {
                    handler(APIResponse(code: response.response!.statusCode, value: response.result.value!))
                } else {
                    handler(APIResponse(value: ["error": "通信に失敗しました"]))
                }
            }
    }
}

enum RequestMethod {
    case GET
    case POST
    case PUT
    case DELETE

    func toAlamofile() -> Alamofire.Method {
        switch self {
        case .GET:
            return .GET
        case .POST:
            return .POST
        case .PUT:
            return .PUT
        case .DELETE:
            return .DELETE
        }
    }
}

struct APIResponse {
    var status: APIStatus
    var value: AnyObject?
    var errorMessage: String?

    enum APIStatus {
        case Success
        case Failure

        func isSuccess() -> Bool {
            switch self {
            case .Success:
                return true
            case .Failure:
                return false
            }
        }
    }
    
    init(code: Int = 0, value: AnyObject) {
        switch code {
        case 200...299:
            status = APIStatus.Success
            self.value = value
        default:
            status = APIStatus.Failure
            self.errorMessage = value["error"] as? String
        }
    }
}

使うとき

リクエスト先となるエンドポイントと、リクエストパラメータを用意します。 response には APIResponse のインスタンスが入ってきます。 response.status.isSuccess() でリクエストに成功したかを判断し、成功 or 失敗 の処理を分けて記述します。

func test() {
    let endpoint = "accounts" // エンドポイント
    let parameters = ["page": 1] // リクエストパラメータ
    APIClient.httpRequest(.GET, endpoint: endpoint, parameters: parameters) { response in
        if response.status.isSuccess() {
            print(response.value!) // 通信に成功したときの処理
        } else {
            print(response.errorMessage!)  // 通信に失敗したときの処理
        }
    }
}