Moya(+RxSwift)のAPIクライアントの一例(Swift5)
前提
SwiftではデフォルトではURLSessionクラスで通信処理を行うが、Moyaはそれのラッパとなるライブラリである。
便利に書ける事から、開発の現場ではスタンダードなやり方と思われる。特にRxSwiftと組み合わせるとさらに便利。
一応、URLSessionクラスもある程度知っておいた方がいい事から、初心者の方はまずURLSessionで慣れた後、Moyaに移行し便利に使える事を実感した方がいいだろう。
公式
Swift4 + Moya + RxSwift + Codableで作るAPIクライアント
全体
APIClient.swift
final class APIClient {
private init() {}
static let shared = APIClient()
// MARK: - Private
private let provider = MoyaProvider<MultiTarget>()
private let stubProvider = MoyaProvider<MultiTarget>(stubClosure: MoyaProvider.immediatelyStub)
// MARK: - Public
func request<G: ApiTargetType>(_ request: G) -> Single<G.Response> {
Single<G.Response>.create { observer in
self.makeRequest(request)
.subscribe(onSuccess: { response in
observer(.success(response))
}, onError: { error in
//プロジェクト全体で共通して行いたいエラーハンドリング等
observer(.error(error))
})
}
}
func makeRequest<G: ApiTargetType>(_ request: G) -> Single<G.Response> {
provider.rx
.request(MultiTarget(request))
.flatMap({ response -> Single<Response> in
// レスポンスヘッダーのチェック
return Single.just(try response.lookingAllHeaderFields())
})
.flatMap { response -> Single<Response> in
// エラーコードのチェック
return Single.just(try response.successfulStatusCodesPolicy())
}
.map(G.Response.self, failsOnEmptyData: false)
}
func requestStub<G: ApiTargetType>(_ request: G) -> Single<G.Response> {
Single<G.Response>.create { observer in
self.makeRequestStub(request)
.subscribe(onSuccess: { response in
observer(.success(response))
}, onError: { error in
if let error = error as? MoyaError {
//プロジェクト全体で共通して行いたいエラーハンドリング等
observer(.error(error))
})
}
}
func makeRequestStub<G: ApiTargetType>(_ request: G) -> Single<G.Response> {
stubProvider.rx
.request(MultiTarget(request))
.flatMap({ response -> Single<Response> in
// レスポンスヘッダーのチェック
return Single.just(try response.lookingAllHeaderFields())
})
.flatMap { response -> Single<Response> in
// エラーコードのチェック
return Single.just(try response.successfulStatusCodesPolicy())
}
.map(G.Response.self, failsOnEmptyData: false)
}
}
ApiTargetType.swift
import Foundation
import Moya
protocol ApiTargetType: TargetType {
associatedtype Response: Codable
}
extension ApiTargetType {
/// The target's base `URL`.
var baseURL: URL {
URL(string: "\(URLs.baseURL)")!
}
}
解説
APIクライアントはシングルトンオブジェクト
- API通信を行うため、MoyaProviderクラスのオブジェクトが通信途中で解放されてしまうなどという事態は避けなくてはならない。APIクライアントを利用する側で、MoyaProviderクラスのオブジェクトを(メンバ変数として設定するなどして)十分長期間保持するということもできる。ここではそのような手間を避けるため、シングルトンオブジェクトとして
shared
を設定し、MoyaProviderがプログラムの実行の全過程を通じて存続するようにしている。
final class APIClient {
private init() {}
static let shared = APIClient()
MultiTargetの使用
- 異なる種類のTargetTypeをMoyaProviderに割り当てられるようにするため、
MultiTarget
を利用している。これにより、APIクライアント利用側ではジェネリクスを通じてMoyaProviderにTargetTypeを渡すという手間が省け、MoyaProviderの存在を利用側に対して隠蔽することができる。
private let provider = MoyaProvider<MultiTarget>()
Pluginの利用
- 必要であれば、MoyaProviderを初期化する際、Pluginというものを渡す事ができる。これはリクエストを送る前、またはレスポンスを受け取った後などに何か副作用を記載したい場合に利用できる。用途としては、公式のコメントに記載があるように、リクエストを送る・レスポンスを受け取るときにログを出すであるとか、インジケータ(ローディング中である事を示すグルグル回るもの)の出し入れを行うとか、リクエストを送る前にURLRequestの設定を変更したい時などに利用できる。
private let provider = MoyaProvider<MultiTarget>(plugins: [FooPlugin()])
/// A Moya Plugin receives callbacks to perform side effects wherever a request is sent or received.
///
/// for example, a plugin may be used to
/// - log network requests
/// - hide and show a network activity indicator
/// - inject additional information into a request
public protocol PluginType {
/// Called to modify a request before sending.
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest
/// Called immediately before a request is sent over the network (or stubbed).
func willSend(_ request: RequestType, target: TargetType)
/// Called after a response has been received, but before the MoyaProvider has invoked its completion handler.
func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType)
/// Called to modify a result before completion.
func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError>
}
stubProviderの利用
- stubProviderを使うと、各TargetTypeに記載してあるサンプルデータをレスポンスとして返す事ができる。スタブの振る舞いとしては.immediatelyStubだと即座にスタブデータを返すが、.delayだと指定した秒数後にデータを返す事ができ、より本番に近い状況でテストしたい時には有効。
private let stubbingProvider = MoyaProvider<MultiTarget>(stubClosure: MoyaProvider.immediatelyStub) //.delayed(seconds: 1.0)等でも可
レスポンスのチェック
- 返ってきたレスポンスにはステータスコード・ヘッダーフィールド・データが含まれているが、これらに対して行いたい処理がプロジェクト全体で共通している場合は、APIクライアントに記載してしまうと良い。エラーハンドリングや、ログを出す等。
// MARK: - Public
func request<G: ApiTargetType>(_ request: G) -> Single<G.Response> {
Single<G.Response>.create { observer in
self.makeRequest(request)
.subscribe(onSuccess: { response in
observer(.success(response))
}, onError: { error in
//プロジェクト全体で共通して行いたいエラーハンドリング等
observer(.error(error))
})
}
}
func makeRequest<G: ApiTargetType>(_ request: G) -> Single<G.Response> {
provider.rx
.request(MultiTarget(request))
.flatMap({ response -> Single<Response> in
// レスポンスヘッダーのチェック
return Single.just(try response.lookingAllHeaderFields())
})
.flatMap { response -> Single<Response> in
// エラーコードのチェック
return Single.just(try response.successfulStatusCodesPolicy())
}
.map(G.Response.self, failsOnEmptyData: false)
}
ターゲットタイプ
プロジェクト全体で共通するもの(典型的には、ベースのURLなど)があれば、くくり出してTargetTypeのプロトコルを作ると良い。
protocol ApiTargetType: TargetType {
associatedtype Response: Codable
}
extension ApiTargetType {
/// The target's base `URL`.
var baseURL: URL {
URL(string: "\(URLs.baseURL)")!
}
}
利用例
func fetchData() -> Single<HogeEntity> {
return APIClient.shared.request(HogeTargetType())
}
import Foundation
import Moya
struct HogeTargetType: ApiTargetType {
typealias Response = HogeEntity
var path: String {
return "/hogehoge/"
}
var method: Moya.Method {
return .get
}
var task: Task {
let param: [String: Any] = [
"id": id
]
return .requestParameters(parameters: param, encoding: URLEncoding.default)
}
var headers: [String: String]? {
[
"Content-Type": "application/json",
"x-auth-token": API.authToken
]
}
var sampleData: Data {
let path = Bundle.main.path(forResource: "Hoge", ofType: "json")!
guard let file = FileHandle(forReadingAtPath: path) else {
return Data()
}
return file.readDataToEndOfFile()
}
// MARK: - Arguments
/// ID
var id: String {
return DataManager.shared.id ?? ""
}
init() {}
}
struct HogeEntity: Codable {
let fuga: String
{
"fuga": "test"
}
RxSwiftを使わない場合
RxSwiftを使わず、 コンプリーションハンドラーでやる場合は、APIClientの一部のコードを以下のように変えればできるかと思います。
func request<G: APITargetType>(_ request: G, completion: @escaping (_ result: Result<G.Response, Error>) -> Void) {
makeRequest(request) { result in
switch result {
case .success(let response):
completion(.success(response))
case .failure(let error):
completion(.failure(error))
}
}
}
private func makeRequest<G: APITargetType>(_ request: G, completion: @escaping (_ result: Result<G.Response, Error>) -> Void) {
provider.request(MultiTarget(request)) { result in
switch result {
case .success(let response):
// レスポンスを目的の型に変換
do {
let mappedResponse = try response.map(G.Response.self)
Logger.info("API success result: ")
self.checkResponse(response)
completion(.success(mappedResponse))
} catch(let error) {
Logger.info("API error result: cannot convert response. error: \(error.localizedDescription)")
self.checkResponse(response)
completion(.failure(error))
}
case .failure(let error):
Logger.info("API error result: \(error.localizedDescription)")
completion(.failure(error))
}
}
}
func requestStub<G: APITargetType>(_ request: G, completion: @escaping (_ result: Result<G.Response, Error>) -> Void) {
makeRequestStub(request) { result in
switch result {
case .success(let response):
completion(.success(response))
case .failure(let error):
completion(.failure(error))
}
}
}
private func makeRequestStub<G: APITargetType>(_ request: G, completion: @escaping (_ result: Result<G.Response, Error>) -> Void) {
stubProvider.request(MultiTarget(request)) { [weak self] result in
guard let `self` = self else { return }
switch result {
case .success(let response):
// レスポンスを目的の型に変換
do {
let mappedResponse = try response.map(G.Response.self)
Logger.info("API success result: ")
self.checkResponse(response)
completion(.success(mappedResponse))
} catch(let error) {
Logger.info("API error result: cannot convert response. error: \(error.localizedDescription)")
self.checkResponse(response)
completion(.failure(error))
}
case .failure(let error):
Logger.info("API error result: \(error.localizedDescription)")
completion(.failure(error))
}
}
}
private func checkResponse(_ response: Response) {
let message = """
response.debugDescription: \(response.debugDescription)
response.data: \(String(describing: String(data: response.data, encoding: .utf8)))
response.statusCode: \(response.statusCode)
response httpResponse.allHeaderFields: \(String(describing: response.response?.allHeaderFields))
response headers: \(String(describing: response.response?.headers))
response url: \(String(describing: response.response?.url))
response.request: \(String(describing: response.request))
"""
Logger.info(message)
}
}
参考資料
Swift4 + Moya + RxSwift + Codableで作るAPIクライアント
Moya/Examples/Multi-Target/ViewController.swift
Author And Source
この問題について(Moya(+RxSwift)のAPIクライアントの一例(Swift5)), 我々は、より多くの情報をここで見つけました https://qiita.com/satoru_pripara/items/6b637ebf7dddd7cbad67著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .