以前 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 にリクエストするRequest
はCodable
に準拠したパースしたい実体 (構造体) が入る- 非同期で動作
- 結果は
Result<Codable, Error>
で返ってくる .get
,.post
などどんなメソッドでもこのApiClient
が担う
という至って普通の API Client だと思います (多分)。
ちなみに、 Impl
は「実装」を意味する “Implementation” から来ています。
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 です。
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
にパースしたい実体を紐付けます。
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 エンジニアなので、最初以下のような空のレスポンスを定義してました。
// この実体を空にすれば成功すると思ってた。。。。😭
struct PostEntity { }
response を定義した構造体を空にすれば Moya が
Moya 「あ、この request はレスポンスないんだね! (従順)」
なんて察してくれると思っていました。。。
が失敗。
Moya 「レスポンスがマッピングできんかったわ。失敗な。一昨日来やがれ。」
って言われました。(言われてない)
成功した対処法
コードからいきなり書いていきますが、後で軽く説明します。
まず、repository に以下のようなメソッドを作ります。
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:
に組み込みます。
// さっき書いたので細かい部分は略
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
で返してくれるメソッド」です。
軽く順序立てて説明してみます。(説明めっちゃ下手でごめん)
- まず、レスポンス body が空のものをパースしようとすると、パースできない (返ってくるパース対象がない) のでエラーとなります。
- これは、 Moya の
map
メソッドで失敗しています。 - この場合のエラーは、
MoyaError.objectMapping
というエラーになります。 - なので、
if case MoyaError.objectMapping(_, let response) = error
によって、ApiClient
から返ってきたエラーがMoyaError.objectMapping
かを判定します。 - 次に、この
MoyaError.objectMapping
はResponse
型引数を持ち、Response
型にはstatusCode: Int
というプロパティが定義されています。 - そして、この
statusCode
で判定すれば良いということになります。 - 今回は、 200 から 204 に入れば成功となるので、
return 200...204 ~= response.statusCode
を返しています。
おわり
これでMoya を使ってレスポンスが空かつ、ステータスコードによって成功・失敗を判断することができました。
でもなんか、そもそも ApiClient
が .failure
を返すのが気に入ってないんですよね。。。
(その場しのぎ感がすごい)
なんかフラグを変えて、ステータスコードによって判定するようにしてくれたりしないのでしょうか。。。
初めて Moya 使ったので、もっといい方法あれば教えて下さい。