0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

WebAPI通信クラスをasync/awaitを用いて書いてみた

Last updated at Posted at 2022-06-30

概要

Swift5.5から利用できるasync/awaitを使い通信クラスを書きました。
Qiitaの記事一覧を取得するAPIを使います。

仕様ではエラーレスポンスが

{
  "message": "Not found",
  "type": "not_found"
}

の形式で返されるのでこちらに対応します。

APIConfigure.swift
import Foundation

enum APIError: Error {
    case server(Int)
    case decode(Error)
    case errorResponse(Error)
    case noResponse
}

enum HttpMethod: String {
    case get = "GET"
    case post = "POST"
}

protocol Requestable {
    associatedtype SuccessModel
    associatedtype FailureModel: Error

    var url: String { get }
    var httpMethod: HttpMethod { get }
    var headers: [String: String] { get }
    var parameters : [String: String] { get }

    func successDecode(from data: Data) throws -> SuccessModel
    func failureDecode(from data: Data) throws -> FailureModel
}

extension Requestable {
    var urlRequest: URLRequest {

        var components = URLComponents(string: url)!
        components.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
        
        let componentsUrl = components.url!
        var request = URLRequest(url: componentsUrl)
        request.httpMethod = httpMethod.rawValue

        headers.forEach { key, value in
            request.addValue(value, forHTTPHeaderField: key)
        }
        
        return request
    }
}


class APIConfigure {
    static func request<T: Requestable>(_ requestable: T) async throws -> T.SuccessModel  {
        let request = requestable.urlRequest
        let (data, response) = try await URLSession.shared.data(for: request, delegate: nil)
        
        guard let response = response as? HTTPURLResponse else {
            throw APIError.noResponse
        }

        if case 200..<300 = response.statusCode {
            do {
                let model = try requestable.successDecode(from: data)
                return model
            } catch let decodeError as DecodingError {
                throw APIError.decode(decodeError)
            }
        } else if response.statusCode >= 500 {
            throw APIError.server(response.statusCode)
        } else {
            do {
                let errorModel = try requestable.failureDecode(from: data)
                throw APIError.errorResponse(errorModel)
            } catch let decodeError as DecodingError {
                throw APIError.decode(decodeError)
            }
        }
    }
}

APIRequest.swift
import Foundation

struct APIRequest {

    /// 記事一覧取得
    struct QiitaArticleItemsAPIRequest: Requestable {
        typealias SuccessModel = [QiitaArticleItem]
        typealias FailureModel = QiitaError

        var url = "https://qiita.com/api/v2/items"
        var httpMethod = HttpMethod.get
        var headers: [String: String] = [:]
        var parameters: [String: String] = [:]

        func successDecode(from data: Data) throws -> [QiitaArticleItem] {
            let decoder = JSONDecoder()
            return try decoder.decode([QiitaArticleItem].self, from: data)
        }

        func failureDecode(from data: Data) throws -> QiitaError {
            let decoder = JSONDecoder()
            return try decoder.decode(QiitaError.self, from: data)
        }
    }
}

QiitaArticleItem.swift
import Foundation

struct QiitaArticleItem: Codable {
    let title: String
    let url: String
    let user: QiitaUser
}

struct QiitaUser: Codable {
    let profile_image_url: String
}

struct QiitaError: Codable, Error {
    let message: String
    let type: String
}
APIClient.swift
import Foundation

class APIClient {

    /// Qiitaの記事取得
    func getQiitaArticleItems(page: Int, perPage: Int) async throws -> [QiitaArticleItem] {
        var request = APIRequest.QiitaArticleItemsAPIRequest()
        request.parameters = ["page": "\(page)",
                              "per_page": "\(perPage)"]
        return try await APIConfigure.request(request)
    }
}
ViewControllerなどで通信する場合
        Task {
            do {
                let articles = try await APIClient().getQiitaArticleItems(page: 1, perPage: page)
                self.articles = articles

                // UITableViewなど使っている場合はデータ更新
                // self.tableView.reloadData()
            } catch {
                // エラー処理

                if let apiError = error as? APIError {
                    switch apiError {
                    case .server(let statusCode):
                        print("statusCode=\(statusCode)")
                    case .decode(let error):
                        print(error.localizedDescription)
                    case .noResponse:
                        print("No Response")
                    case .errorResponse(let error):
                        if let error = error as? QiitaError {
                            let alert = UIAlertController(title: "error", message: "Message=\(error.message)\nType=\(error.type)\n", preferredStyle: .alert)
                            let ok = UIAlertAction(title: "OK", style: .default) {_ in
                                self.dismiss(animated: true, completion: nil)
                            }
                            alert.addAction(ok)
                            present(alert, animated: true, completion: nil)
                        } else {
                            print(error.localizedDescription)
                        }
                    }
                } else {
                    print(error.localizedDescription)
                }
            }
        }

悩んだポイントorわからないこと

  • 成功時、エラー時でレスポンスのjsonの構造が違うためsuccessDecode(), failureDecode()と分けたがなんか気持ち悪い

  • エラー処理をdo-catch文で対処するため、エラーレスポンス受信時はデコードしたデータモデルをthrowしている。throwするためQiitaErrorの構造体はErrorを継承している。失敗は失敗だが、例外として処理するのは正しいのだろうか?

  • 例外としてではなくResultを使って成功と失敗を分けたいがやり方がわからなかった。

  • デコード失敗時を} catch let decodeError {で書いた場合例外を全部拾ってしまうため} catch let decodeError as DecodingError {と書く必要があった。
    こちらは気づかず解決に時間がかかった。

あとがき

Task, do ってやるとネストが深くなっていくような...
実際の業務だったら、たぶんまだAlamofireを使う。

参考

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?