解せぬ日記

雑な話をする

Alamofireで204 No ContentなAPIのレスポンスをどう扱うか

https://media.giphy.com/media/aWjpyBmKXQRAA/giphy.gif

TL;DR

現状、SwiftのHTTPクライアントであるAlamofireで204 No ContentなAPIの処理のベストプラクティスは存在しないので、responseで受けるか、Response Serializerを作るか、自分でResponseを作って泥臭くやるしかない。 Response Serializerを作るのが、他のAPIの処理と同じように実装できていいように僕は思う。

背景

Swift 2.0とiOS 9対応をしていて、Alamofireのバージョンが上がったことで、AlamofireがResponse(Struct)とResult(Enum)を持つようになった。

それに伴い、responseJSONresponseStringなどはデータが返ってくることを期待した実装に変更され、通信完了時のハンドラにResultを含むResponseが渡されるようになった。 (以前、使っていたバージョンではそういったものはなく、Request, Response, Data, ErrorがOptionalで渡される実装だったので、自前でResponseを定義して扱っていた。)

// 以前: ハンドラにrequest, response, json, errorのOptionalが渡されている
Alamofire.request(urlRequest)
    .validate(statusCode: 200..<300)
    .responseJSON { (request, response, json, error) in
        // code     
     }

// 現在: ハンドラにResponse<AnyObject, NSError>が渡される
Alamofire.request(urlRequest)
    .validate(statusCode: 200..<300)
    .responseJSON { response in
        // code     
     }

response以外の実装ではデータが返ってくることを期待しているので、204 No Contentの場合にシリアライズエラーとなって処理が失敗したことになってしまう。

対策

対策は幾つか思いつく。

  • responseresponseJSONresponseStringから呼び出されるメソッドで、通信完了時に呼び出されるハンドラにRequest, Response, Data, ErrorがOptionalで返ってくるので、他のAPIとは別の方法でデータを処理する
  • responseとAlamofireのResponse、Resultを組み合わせて、他のAPIと同じ処理ができるようにする

けど上記の方法はいずれもアドホックな対応になり、微妙な気がしてやめた。

Issueを巡っていると、コミッタの人が例えばNo Dataを扱うResponse Serializerなんかあったらどうかみたいな話をしていて、Response Serializer作れることが分かった。 Response Serializerを作るとresponseJSONと同じようにレスポンスを扱えるので便利そうだし、作るぞ!!という気持ちになったので作った。

github.com

実装

ドキュメントにXMLのResponse Serializerのサンプルがあったので、これを参考にした。

https://github.com/Alamofire/Alamofire#creating-a-custom-response-serializer

extension Request {
    public static func NoContentResponseSerializer() -> ResponseSerializer<AnyObject, NSError> {
        return ResponseSerializer { request, response, data, error in
            guard error == nil else { return .Failure(error!) }

            guard let _ = data else {
                let failureReason = "Data should not exist. Input data should nil."
                let error = Error.errorWithCode(.DataSerializationFailed, failureReason: failureReason)
                return .Failure(error)
            }

            return .Success("")
        }
    }

    public func responseNoContent(completionHandler: Response<AnyObject, NSError> -> Void) -> Self {
        return response(responseSerializer: Request.NoContentResponseSerializer(), completionHandler: completionHandler)
    }
}

.Success("")で空文字を返してるのはちょっとした理由があって、responseJSONResponse<AnyObject, NSError>を返してくるので、そこを揃えたかったため。

本来であればnilを入れて返したいところで、もうちょっとうまい実装があればOptionalとして返したりできそうだが、OptionalとAnyObjectを同じように扱うにはAnyを使う必要があって、現時点ではSwfit 2.0とiOS 9対応を優先させて、とりあえずこれで動くようにしてる。

あとは呼び出し側で、

Alamofire.request(urlRequest)
    .validate(statusCode: [204])
    .responseNoContent { response in
        // code
    }

のように呼び出せば良い。

まとめ

他にいい方法があったり、こんなやり方イケてないだろって話があれば、ぜひ教えていただきたく、何卒。 AnyObjectは割とよく使うけど、Anyって結構使うもんなんだろうか…。