[Swift] URLSession+async/await+CodableでAPIクライアントを作る


**※注意**
**本記事は Xcode 13.1 beta の環境で動作確認したものです。正式版では動作が変わる可能性もあります。**


# 前書き

過去に以下のような記事を上げていましたが、その async/await 版になります。

- [[Swift] ObjectMapper+RxSwiftを実装した備忘録](https://qiita.com/H_Crane/items/ac3b372d969e4ec1714e)
- [[Swift] Alamofire+RxSwift+CodableでAPIクライアントを作る](https://qiita.com/H_Crane/items/d2d316b086745417ffae)
- [[Swift] URLSession+Combine+CodableでAPIクライアントを作る](https://qiita.com/hcrane/items/4778d7951cd0e1611f5b)

そのまんま置き換えというのは、**厳しい部分がありました**がなんとか同じような書き方に集約しております。

# 導入

## 1.環境

```
- MacOS Big Sur
- Xcode 13 Beta1
```

エンコードの実装を考えたくないので、``Alamofire`` の機能を使用します。（個々に入れてください）
また、入れずにエンコードを自前で実装しても構いません。

### Carthageのエラー
Xcode13 beta1 でCarthageエラーが起こる場合は、回避するスクリプト（下記``carthage.sh``）で対応できます。

https://qiita.com/temoki/items/e3c05ee7e0f7a37b87e9

## 2.ネットワークプロトコルの作成

API ごとのリクエストを作成、雛形 ``Protocol`` を作成します。

こちらに関しては、上記でもあげた以下
- [[Swift] ObjectMapper+RxSwiftを実装した備忘録](https://qiita.com/H_Crane/items/ac3b372d969e4ec1714e)
- [[Swift] Alamofire+RxSwift+CodableでAPIクライアントを作る](https://qiita.com/H_Crane/items/d2d316b086745417ffae)

にほぼ同じ記載があるので、解説を読みたい方はこちらを読んでください。
ここではコードのみ置いておきます。

### - 基底のプロトコル

```.swift
protocol BaseAPIProtocol {
    associatedtype ResponseType

    var method: HTTPMethod { get }
    var baseURL: URL { get }
    var path: String { get }
    var headers: [String : String]? { get }
}

extension BaseAPIProtocol {

    var baseURL: URL {
        return try! "http://localhost:3000".asURL() // エンドポイント例
    }

    var headers: [String : String]? { // ヘッダー例
        return [
            "Content-Type": "application/json; charset=utf-8",
            "User-Agent": ""
        ]
    }
}
```

### - リクエストのプロトコル

```.swift
protocol BaseRequestProtocol: BaseAPIProtocol, URLRequestConvertible {
    var parameters: Parameters? { get }
    var encoding: URLEncoding { get }
}

extension BaseRequestProtocol {
    var encoding: URLEncoding {
        return URLEncoding.default
    }

    func asURLRequest() throws -> URLRequest {
        var urlRequest = URLRequest(url: baseURL.appendingPathComponent(path))
        urlRequest.httpMethod = method.rawValue
        urlRequest.allHTTPHeaderFields = headers
        urlRequest.timeoutInterval = TimeInterval(30)
        if let params = parameters {
            urlRequest = try encoding.encode(urlRequest, with: params)
        }
        return urlRequest
    }
}
```


## 3.リクエスト・レスポンスの作成

API ごとのリクエスト・レスポンスを作成します。

こちらに関しては、上記でもあげた以下
- [[Swift] URLSession+Combine+CodableでAPIクライアントを作る](https://qiita.com/hcrane/items/4778d7951cd0e1611f5b)

に全く同じ記載があるので、解説を読みたい方はこちらを読んでください。



## 4.ネットワーククライアントの作成

実際に作成したコードになります。
（以下、``@available(iOS 15, *)``は全て省略しています。）

```.swift
struct APICliant {
    
    // MARK: Variables

    private static let successRange = 200..<300
    private static let decoder: JSONDecoder = {
        let jsonDecoder = JSONDecoder()
        jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
        return jsonDecoder
    }()
    
    
    // MARK: Method
    
    // APIの呼び出し
    static func call<T, V>(_ request: T) async throws -> V
        where T: BaseRequestProtocol, V: Codable, T.ResponseType == V {
        
        let result = try await URLSession.shared.data(for: request.asURLRequest())
        let data = try validate(data: result.0, response: result.1)
        return try decoder.decode(V.self, from: data)
    }
    
    // ネットワークの精査
    static func validate(data: Data, response: URLResponse) throws -> Data {
        guard let code = (response as? HTTPURLResponse)?.statusCode else {
            throw NSError(domain: String(data: data, encoding: .utf8) ?? "Network Error", code: 0)
        }
        guard successRange.contains(code) else {
            throw NSError(domain: "out of statusCode range", code: code)
        }
        return data
    }
}
```

ステータスコードをより詳細にハンドリングしたいのであれば、分岐してあげると良いでしょう。（例として）

```.swift
static func validateCode(data: Data, response: URLResponse) throws -> Data {
    switch (response as? HTTPURLResponse)?.statusCode {
    case .some(let code) where code == 401:
        throw NSError(domain: "Unauthorized", code: code)
        
    case .some(let code) where code == 404:
        throw NSError(domain: "Not Found", code: code)
        
    case .none:
        throw NSError(domain: "Irregular Error", code: 0)
        
    case .some:
        return data
    }
}
```

## 5. 使用例

とてもスッキリしています。

```.swift
Task {
    let request = UserRequest()
    let response = try await APICliant.call(request)
    debugPrint(response)
}
```

~~``await`` のメソッドを呼び出すためには、``async`` で明示的に囲う必要があります。
他にも ``asyncDetached``、``detach`` などがあります。~~

``await`` のメソッドを呼び出すためには、``Task`` で明示的に囲う必要があります。
今回は説明しませんが、この辺を読んでおくと理解が深まると思います。

https://zenn.dev/akkyie/articles/swift-concurrency



# 実用例

具体的に２つ出してみます。

## 1. 実装

### ① SwiftUI の実装例

![CASE1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/d5492aec-caab-22da-8f93-ca61004572c0.png)

``SwiftUI`` での簡単な呼び出し例を紹介します。

#### - UseCase

まずは ``UseCase`` を作ります。

```.swift
final class APIUseCase {
    
    @Published var users = [UserModel]()
    
    func fetch() async {
        do {
            let request = UserRequest()
            let response = try await APICliant.call(request)
            users = response.data
        } catch let error {
            debugPrint(error.localizedDescription)
        }
    }
}
```

メソッドの後ろに ``async``を付けたものでラップしています。
結果を ``@Published`` なプロパティでバインディングします。

（``UseCase`` という名前ですが、``ViewModel`` と同じだと思っていただいて構いません。）

#### - View

```.swift
struct ContentView:  View {
    
    private var useCase = APIUseCase()
    
    var body: some View {
        EmptyView()
            .task {
                await useCase.fetch()
            }
            .onReceive(useCase.$users) { users in
                print("users: ", users)
            }
    }
}
```

iOS15 から使えるようになった ``task`` を使用することで ``async`` で囲う必要がなくなります。
以下の説明にある通り、画面が表示された際に実行されます。
![task](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/ebe2df23-5574-f105-717e-19c6282317a2.png)
（引用：[Developer - task(_:)](https://developer.apple.com/documentation/swiftui/emptyview/task(_:))）

### ② Protocol で縛った実装例

![case2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/24613958-6d1f-d4d9-19ff-799ae6d6f0b8.png)

``@Published`` を使用する場合、``Protocol`` で縛れないので ``Combine`` を使っていきます。

#### - Inputs/Outputs

``MVVM`` を実装する際に良く見られるパターンですが、入力と出力を``Protocol`` で縛っていきます。

```.swift
// 入力と出力の制限
protocol APIUseCasable {
    var inputs: APIUseCaseInputs { get }
    var outputs: APIUseCaseOutputs { get }
}

// 入力 （ View → UseCase ）
protocol APIUseCaseInputs {
    func fetch() async
}

// 出力 （ UseCase → View ）
protocol APIUseCaseOutputs {
    var users: PassthroughSubject<[UserModel], Never> { get set }
}
```

#### - UseCase

作ったものを適応した ``UseCase`` を作成します。

```.swift
final class APIUseCase: APIUseCasable, APIUseCaseInputs, APIUseCaseOutputs {
    
    // MARK: APIUseCasable
    var inputs: APIUseCaseInputs { self }
    var outputs: APIUseCaseOutputs { self }
    
    // MARK: APIUseCaseOutputs
    var users = PassthroughSubject<[UserModel], Never>()
    
    // MARK: APIUseCaseInputs
    func fetch() async {
        do {
            let request = UserRequest()
            let response = try await APICliant.call(request)
            users.send(response.data)
        } catch let error {
            debugPrint(error.localizedDescription)
        }
    }
}
```

``inputs``と``outputs`` を通してのみ、メソッドの実行や値のやりとりを行うことが保証されます。
また、``Combine``の ``PassthroughSubject`` を使用して、``View`` 側に値を返します。　

#### - View

```.swift
struct ContentView:  View {
    
    private var useCase: APIUseCasable = APIUseCase()
    
    var body: some View {
        EmptyView()
            .task {
                await useCase.inputs.fetch()
            }
            .onReceive(useCase.outputs.users) { users in
                print("users: ", users)
            }
    }
}
```

``UseCase`` プロパティを ``Protocol`` (APIUseCasable) で定義することで縛ります。
実行では ``inputs`` を通し、値を受け取る側では ``outputs`` を通しています。

この方法だと、``UseCase``はそのまま ``UIKit`` でも使うことができるメリットもあります。

（ただし、``SwiftUI`` 特有の ``Binding`` が必要な場合は、``onReceive`` で受け取ってから``View``のプロパティに再度バインディングさせるといったひと工夫が必要になります）

# あとがき

``Combine`` が出てきたと思えば、あっという間に新しい技術が出てきてしまいました。
（議論は前からされていたけど...）

まだ、見切れていない部分もあるので、今回記載したものよりもっと良い実装ができるかもしれません。

コードの指摘等あればお願いしますmm
動作するリポジトリを置いておきます

https://github.com/crane-hiromu/URLSession-AsyncAwait-Codable