2
1

More than 1 year has passed since last update.

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

Posted at

やること

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

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を使用するときに必要な情報を記述します。

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

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()を返しています。

参考

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