前回、Combineでエラーハンドリングとかも含めた比較的丁寧な通信処理を書いたのですが、
async await
について今更ながら勉強したし、比較も兼ねて、async await
バージョンも書いてみました。
前回のCombine版はこちら!
Request/Responseの共通化に関する説明はこちらの記事にしか載せてないので、その部分を知りたい方はぜひこちらの記事をご覧いただけると幸いです。
async awaitについて
- Swift5.5から使えるようになった新しい技術
-
async
await
キーワードを使うことで、非同期処理についてネストが深くならないような書き方ができる -
completion
を使う方法だと複雑な処理になったときに、completion
忘れ、コードを追うのが大変になるが、async await
はその心配がない - コンパイラーによるコードチェックが手厚い
などのメリットがあります。
こちらの解説サイトが個人的にわかりやすく、大変参考になりました。
完成コード
public protocol APIClientProtocol: AnyObject {
func send<T: RequestProtocol>(_ request: T) async throws -> T.Response
}
public final class APIClient: APIClientProtocol {
private init() {}
public static let shared = APIClient()
public func send<T: RequestProtocol>(_ request: T) async throws -> T.Response {
guard let url = URL(string: request.baseURL)?.appendingPathComponent(request.path) else {
throw APIError.failedToCreateURL
}
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
throw APIError.failedToCreateComponents(url)
}
components.queryItems = request.parameters?.compactMap {
.init(name: $0.key, value: "\($0.value)")
}
guard var urlRequest = components.url.map({ URLRequest(url: $0) }) else {
throw APIError.failedToCreateURL
}
urlRequest.httpMethod = request.method.rawValue
urlRequest.allHTTPHeaderFields = [
"Content-Type": "application/json"
]
do {
let (data, urlResponse) = try await URLSession.shared.data(for: urlRequest)
// responseのチェック
guard let urlResponse = urlResponse as? HTTPURLResponse else {
throw APIError.noResponse
}
// HTTPステータスコードのチェック
guard 200 ..< 300 ~= urlResponse.statusCode else {
throw APIError.unacceptableStatusCode(
urlResponse.statusCode
)
}
let decodedData = try JSONDecoder().decode(T.Response.self, from: data)
// Responseデータのstatus項目が正常値であることをチェック
guard decodedData.status == "ok" else {
throw APIError.responseStatusError
}
return decodedData
} catch {
if let error = error as? DecodingError {
// Decodeエラーのハンドリング
throw APIError.parserError(error.localizedDescription)
} else if let error = error as? APIError {
// 上流のPublisherでエラーが発生していればここで返す
throw error
} else {
throw APIError.unknown(error.localizedDescription)
}
}
}
}
今回はこちらのAPIに対してリクエストを行う想定で書いています。
どっちも書いてみた感想
エラーハンドリングの面では個人的にはCombineのが使いやすかった
async await
については、エラーをthrow
させることで発生させるのですが、今回の自分のケースのように、APIError
などの自分自身で定義したエラーを返す場合に困り事がありました。
throw
で返した呼び出し元のメソッドでは、Error
型として扱われるため、呼び出し元のメソッドでCombineの時は不要だったエラーハンドリングを書かないといけなくなりました。
さらにいうと、CleanArchitectureなどで、
- DataLayerのエラーを
APIError
- DomainLayerのエラーを
APPError
のような形で定義している場合、throwで返すたびに、カスタムエラー型がError
型にダウンキャストされてしまい、その度にエラーを使うところでキャストするというのは少し面倒になる気がします。
async awaitでは、AnyCancellableで非同期処理を保持したり、[weak self]を書かなくていいことがメリット
Combine
を使った場合のデメリットにもなるのですが、非同期処理を呼び出す場合は以下のようなことをしないといけません。
-
private var cancellables = Set<AnyCancellable>()
を定義する - 非同期処理のResponseの処理などのところが
[weak self]
地獄にならなそう -
.receive(on: DispatchQueue.main)
などの書き忘れでクラッシュなんていう事故にならなそう
これらはCombineに限った話ではなく、「クロージャによる非同期処理」「RxSwiftでの非同期処理」にも共通する問題かもしれませんが、非同期処理を実行するときは、これらに注意して開発したり、これらを忘れるだけで思わぬバグに遭遇したりしますね。
今回のasync await
版は、間違った書き方をするとコンパイルエラーになりますし。
そして、非同期をクロージャで書かなくていいので、今まで必要だった、処理とは直接関係のないコードを書かなくていいのは大きなメリットだなと思いました。
メソッドチェーンが好きか嫌いかで好みが分かれそう
Combine
で実装すると、.flatMap
.tryMap
などでメソッドチェーンして処理を書いていくことになります。
今回のasync await
のケースでは、do catch
によって、ハンドリングしたり、decodeされた値をプロパティとして保持したりするので、書き方の好みとかも影響するのかなと思いました。
(慣れているからかもしれませんが)自分はメソッドチェーンでゴリゴリ書かれている方が見やすいなと思ってしまいます。
おわりに
今回は、async await
を使ったAPI通信処理について書かせていただきました。
Combineと比較してみましたが、お互いに長所・短所がありそうですね。個人的にはどちらも理解して、状況によって使い分けていきたいなと思います。
今回は該当ケースではないので、本題では触れなかったのですが、直列や並列で大量のメソッドを呼び出すとなると、ネストが深くなるので、その場合は、async await
が真価を発揮しそうだなと思います👍
ではまた👋