6
1

More than 1 year has passed since last update.

[iOS][Swift5.5]async/awaitを使った通信処理のサンプル実装

Posted at

URLSession

Foundation.URLSessionにasync/awaitを利用した関数が用意されているのでそれを活用する

公式ドキュメントから引用
    /// Convenience method to load data using an URL, creates and resumes an URLSessionDataTask internally.
    ///
    /// - Parameter url: The URL for which to load data.
    /// - Parameter delegate: Task-specific delegate.
    /// - Returns: Data and response.
    public func data(from url: URL, delegate: URLSessionTaskDelegate? = nil) async throws -> (Data, URLResponse)
        let (data, _) = try await URLSession.shared.data(from: url)

この書き方でコールバックやDelegateを使わずに非同期で通信結果を取得することができる

サンプルコードの概要

QiitaAPIから最新記事を取得する処理を書いてみます。

  1. 最新記事一覧取得
  2. 取得した一覧の中から最新の記事IDを使用してその記事の詳細を取得

APIClientを作ってみる

Networking.swift
...
/// 通信とJSONデータのデコード処理を行う
class Networking {
    static func request<D: Decodable>(url: URL, type: D.Type) async throws -> D {
        var request = URLRequest(url: url)
        let (data, _) = try await URLSession.shared.data(for: request)
        return try JSONDecoder().decode(D.self, from: data)
    }
}
Article.swift
...
/// 記事一覧のアイテムのモデル
struct Article: Decodable {
    let id: String
    let title: String
}
ArticleDetail.swift
...
/// 記事詳細のモデル
struct ArticleDetail: Decodable {
    let title: String
    let body: String
}
APIClient.swift
...
/// 利用するAPIの定義とそれぞれの取得処理まで行う
class APIClient {
    private static let baseURL = "https://qiita.com"
    private enum APIType  {
        case articles(Int)
        case articleDetail(String)

        var path: String {
            switch self {
            case .articles(let perPage):
                return "/api/v2/items?per_page=\(perPage)"
            case .articleDetail(let itemId):
                return "/api/v2/items/\(itemId)"
            }
        }

        var url: URL {
            URL(string: APIClient.baseURL + path)!
        }
    }

    /// 記事一覧取得
    func fetchArticles(perPage: Int = 1) async throws -> [Article] {
        try await Networking.request(url: APIType.articles(perPage).url, type: [Article].self)
    }

    /// 記事詳細取得
    func fetchArticleDetail(itemId: String) async throws -> ArticleDetail {
        try await Networking.request(url: APIType.articleDetail(itemId).url, type: ArticleDetail.self)
    }

}

呼び出し側

ArticleViewModel.swift
...
class ArticleViewModel {
    private let apiClient: APIClient

    init(apiClient: APIClient = APIClient()) {
        self.apiClient = apiClient
    }

    func update() {
        Task {
            do {
                let articles = try await self.apiClient.fetchArticles()
                print(articles)
                guard let id = articles.first?.id else {
                    return
                }
                let article = try await self.apiClient.fetchArticleDetail(itemId: id)
                print(article)
            } catch {
                print(error)
            }
        }
    }
}

printの実行結果↓

[Sample.Article(id: "f1d1a4c72e663d0617dc", title: "【Ruby】初心者がRuby on Railsをやる!")]
ArticleDetail(title: "【Ruby】初心者がRuby on Railsをやる!", body: "# :star:概要:star:\n\n`Ruby`初心者の僕が`Ruby on Rails`までやって...(省略)

感想

Task {}や do {}で囲む必要はありますが以前のやり方であればコールバックのネストが深くなってしまったり処理の流れが非常に追いにくくなってしまっていたと思います。
completion:@escaping (Article)みたいな複雑な(?)記述をしなくても良くなるというのもありがたいです。
エラー処理や通信のキャンセル処理やリトライ処理なども以前より楽に書くことができるようなので今後も色々な場面で使うことになるだろうと思います。

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