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