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

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

[仕様](https://qiita.com/api/v2/docs)ではエラーレスポンスが
```json
{
  "message": "Not found",
  "type": "not_found"
}
```
の形式で返されるのでこちらに対応します。


```swift: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)
            }
        }
    }
}

```

```swift: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)
        }
    }
}

```

```swift: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
}
```

```swift: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)
    }
}
```

```swift: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を使う。

### 参考
https://dev.classmethod.jp/articles/webapi_client_not_use_liblary_swift/