LoginSignup
3
2

More than 1 year has passed since last update.

URLSession × async await で通信処理を書いてみる

Posted at

前回、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が真価を発揮しそうだなと思います👍

ではまた👋

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