6
2

More than 3 years have passed since last update.

Moya を使ってレスポンス body が空の時、ステータスコードによって成功・失敗を判断する方法

Last updated at Posted at 2020-06-22

以前初めて Moya を使った時に、

  • API からステータスコードのみが送られる
  • レスポンスの body は空である

というケースに出会いました。
僕は、「はいはい、どうせ空のレスポンスを作って上げれば .success 返ってくるんでしょ」を思って実装したのですが、、、
テストしてみると全然成功してくれない。。。

こんなときの対処法を紹介します。
「こうすればもっとシンプルになるよ!」という意見あったら下さい!!!!!

最初にざっくり結論

今から、実装クラスを紹介してから対処法を紹介しますが、すぐ知りたいせっかちな方のために結論を。

  • パースを失敗させる
  • すると、MoyaError.objectMapping(Swift.Error, Response) という Error になる
  • この Response には response プロパティがある
  • そして、 response プロパティには statusCode: Int があるからそれを見ればいい!!

API の仕様

  • 成功時: status code: 200 – 204 が返ってくる
  • body: none である
  • post メソッド

という API です。

実装するクラス

まずは API Client から。
ApiClient という型を定義し、 ApiClientImpl がそれに準拠する実装クラスになっています。
Moya を使ったこと無い方はわかりにくいところもあると思いますが、

  • request メソッドで API にリクエストする
  • RequestCodable に準拠したパースしたい実体 (構造体) が入る
  • 非同期で動作
  • 結果は Result<Codable, Error> で返ってくる
  • .get, .post などどんなメソッドでもこの ApiClient が担う

という至って普通の API Client だと思います (多分)。
ちなみに、 Impl は「実装」を意味する "Implementation" から来ています。

ApiClient.swift

import Moya

protocol ApiClient {
    var provider: MoyaProvider<MultiTarget> { get set }
}

class ApiClientImpl: ApiClient {
    func request<Request: ApiTargetType>(_ request: Request, completion: @escaping (Result<Request.Response, Error>) -> Void) {
        let target = MultiTarget(request)
        self.provider.request(target) { result in
            switch result {
            case .success(let response):
                let jsonDecoder = JSONDecoder()
                jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
                jsonDecoder.dateDecodingStrategy = .iso8601
                do {
                    let decodedResponse = try response.map(Request.Response.self, using: jsonDecoder)
                    completion(.success(decodedResponse))
                } catch {
                    completion(.failure(error))
                }
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
}

次にこの ApiClientImpl を呼び出す repository です。

Repository.swift

import Moya

class Repository {
    var apiClient: ApiClient = ApiClientImpl()

    func post(completion: @escaping (Result<PostTarget.Response, Error>) -> Void) {
        let request = PostTarget()
        apiClient.request(request) { result in
            switch result {
            case .success:
                completion(.success(.init()))
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
}

(ネスト深いのは許してくれ。。。。)

最後に PostTarget です。
URL や response, Moya のメソッドなどを定義しています。
もし GET メソッドであれば、この中の Response にパースしたい実体を紐付けます。

PostTarget.swift

import Moya

struct  PostTarget: TargetType {
    typealias Response = PostEntity

    var baseURL: String {
        return "base.url"
    }

    var path: String {
        return "path/to/api"
    }

    var method: Moya.method {
        return .post
    }

    var parameters: [String: Any] {
        return ["testParam1": "testContent"]
    }
}

やってはいけない対処法 (失敗例)

まず、body: none なのでマッピングはできません。
僕はよわよわ iOS エンジニアなので、最初以下のような空のレスポンスを定義してました。

PostEntity.swift

// この実体を空にすれば成功すると思ってた。。。。😭
struct PostEntity { }

response を定義した構造体を空にすれば Moya が
Moya 「あ、この request はレスポンスないんだね! (従順)」
なんて察してくれると思っていました。。。

が失敗。
Moya 「レスポンスがマッピングできんかったわ。失敗な。一昨日来やがれ。」
って言われました。(言われてない)

成功した対処法

コードからいきなり書いていきますが、後で軽く説明します。
まず、repository に以下のようなメソッドを作ります。

Repository.swift

extension Repository {
    private func isSuccess(error: Error) -> Bool {
        if case MoyaError.objectMapping(_, let response) = error {
            return 200...204 ~= response.statusCode
        } else {
            return false
        }
    }
}

(なにやら response.statusCode とやらを比較していますねぇ)

そしてこれを、repository エラーハンドリング部の内、case .failure: に組み込みます。

Repository.swift

// さっき書いたので細かい部分は略
apiClient.request(request) { result in
    switch result {
        case .success:
            completion(.success(.init()))
        case .failure(let error):
            if self.isSuccess(error: error) {
                completion(.success(.init()))
            } else {
                completion(.failure(error))
            }
        }
    }
}

これで、body が空でも、 repository がステータスコードを判定して .success を投げてくれるようになりました!!🎉

何をしているか

isSuccess(error: Error) -> Bool メソッドは、「ApiClient がエラーを投げてきても、ステータスコードから判断して成功 or 失敗を Bool で返してくれるメソッド」です。
軽く順序立てて説明してみます。(説明めっちゃ下手でごめん)

  1. まず、レスポンス body が空のものをパースしようとすると、パースできない (返ってくるパース対象がない) のでエラーとなります。
  2. これは、 Moya の map メソッドで失敗しています。
  3. この場合のエラーは、 MoyaError.objectMapping というエラーになります。
  4. なので、 if case MoyaError.objectMapping(_, let response) = error によって、ApiClient から返ってきたエラーが MoyaError.objectMapping かを判定します。
  5. 次に、この MoyaError.objectMappingResponse 型引数を持ち、Response 型には statusCode: Int というプロパティが定義されています。
  6. そして、この statusCode で判定すれば良いということになります。
  7. 今回は、 200 から 204 に入れば成功となるので、return 200...204 ~= response.statusCode を返しています。

おわり

これでMoya を使ってレスポンスが空かつ、ステータスコードによって成功・失敗を判断することができました。

でもなんか、そもそも ApiClient.failure を返すのが気に入ってないんですよね。。。
(その場しのぎ感がすごい)
なんかフラグを変えて、ステータスコードによって判定するようにしてくれたりしないのでしょうか。。。

初めて Moya 使ったので、もっといい方法あれば教えて下さい。

6
2
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
6
2