Moya ✖️ CombineでAPI通信してみる

## やること
Combineを用いたiOS開発でWebAPI通信を簡単に実装する。

## 実装概要
Moyaを使用するので、CocoaPodsでインストールします。
`pod 'Moya/Combine', '~> 15.0'`

QiitaのAPIを使用するので、Qiitaでトークンを発行しておきます。
設定 > アプリケーション > 個人用アクセストークン で作成できます。

次に実装ですが、まず複数のAPIを使用できるようにAPIClientを実装します。
APIを呼び出すときの共通処理をここに記述します。
また、今回はQiitaのAPIのみ使用しますが、例えばTwitterのAPIも同じアプリ内で使用したい場合などにもすぐに対応できるようにします。

そして、実際に呼び出すエンドポイントなどを記述するQiitaAPIを実装します。
ここにQiitaのAPIを使用するために必要なトークン情報なども記述していきます。

最後にViewModelで、QiitaAPIを呼び出し、記事を取得します。

## APIClinet

```swift
import Moya
import Combine
import Foundation

final class APIClient {
    static let shared = APIClient()
    private init() {}

    private let provider = MoyaProvider<MultiTarget>()
    private let stubbingProvider = MoyaProvider<MultiTarget>(stubClosure: MoyaProvider.immediatelyStub)

    lazy var decoder: JSONDecoder = {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        return decoder
    }()

    func request<T: Decodable>(target: TargetType) -> AnyPublisher<T, Error> {
        return self.provider.requestPublisher(MultiTarget(target))
            .map { $0.data }
            .decode(type: T.self, decoder: decoder)
            .eraseToAnyPublisher()
    }

    func requestStub<T: Decodable>(target: TargetType) -> AnyPublisher<T, Error> {
        return self.stubbingProvider.requestPublisher(MultiTarget(target))
            .map { $0.data}
            .decode(type: T.self, decoder: decoder)
            .eraseToAnyPublisher()
    }
}
```

`func request`は実際にAPIを呼び出すときに使います。
`func requestStub`は自分で作ったMockのデータをレスポンスとして受け取りたい場合に使います。
これらはどのAPIを使用するときにも使う共通の処理です。

## QiitaAPI
QiitaのAPIを使用するときに必要な情報を記述します。

```swift
protocol QiitaAPITargetType: TargetType {
}

extension QiitaAPITargetType {
    private var token: String { "YOUR_QIITA_TOKEN" }
    var baseURL: URL { URL(string: "https://qiita.com/api/v2")! }
    var headers: [String: String]? {
        return [
            "Content-Type": "application/json",
            "Authorization":"Bearer \(token)"
        ]
    }
}

protocol IQiitaAPI {
    func loadItems(data: QiitaArticlesRequestData) -> AnyPublisher<[ArticleDTO], Error>
}

class QiitaAPI: IQiitaAPI {
    private let apiClient = APIClient.shared

    func loadItems(data: QiitaArticlesRequestData) -> AnyPublisher<[ArticleDTO], Error> {
        return apiClient.request(target: QiitaArticlesRequest(data))
    }
}

extension QiitaAPI {
    struct QiitaArticlesRequest: QiitaAPITargetType {
        var path: String { "/items" }
        var method: Moya.Method { .get }
        var validationType: ValidationType { .successCodes }
        var task: Task {
            .requestParameters(parameters: data.toParams(), encoding: URLEncoding.queryString)
        }

        var sampleData: Data { Data() }

        let data: QiitaArticlesRequestData
        init(_ data: QiitaArticlesRequestData) {
            self.data = data
        }
    }
}
```
QiitaAPITargetTypeを作り、QiitaAPIのアクセストークンなどを記述します。
次に、QiitaAPI本体でAPIを呼び出すメソッドを用意します。
QiitaAPIのextensionでどのエンドポイントをどのHTTPメソッドを使って、どのデータをリクエストに含めるかなどの情報を1つのTargetにつき1つのstructを作り、記述します。


## ViewModel
```swift
init(qiitaAPI: IQiitaAPI) {
    subject
        .flatMap { [unowned self] in
            qiitaAPI.loadItems(data: request)
                .catch { [weak self] error -> Empty in
                    self?.errorDescription = error.localizedDescription
                    self?.isShowError = true
                    return Empty()
                }
        }
        .sink { [weak self] items in
            self?.page += 1
            self?.items = items
        }
        .store(in: &cancellables)
}

```
ViewModelでQiitaAPIに作成した`loadItems`を呼び出します。
エラーが発生しても購読が継続されるように、catchのスコープでEmpty()を返しています。

## 参考
https://qiita.com/satoru_pripara/items/6b637ebf7dddd7cbad67

https://qiita.com/toya108/items/e5fb8b3b531c4011d5c7