Moya(+RxSwift)のAPIクライアントの一例(Swift5)


前提

SwiftではデフォルトではURLSessionクラスで通信処理を行うが、Moyaはそれのラッパとなるライブラリである。

便利に書ける事から、開発の現場ではスタンダードなやり方と思われる。特にRxSwiftと組み合わせるとさらに便利。

一応、URLSessionクラスもある程度知っておいた方がいい事から、初心者の方はまずURLSessionで慣れた後、Moyaに移行し便利に使える事を実感した方がいいだろう。

公式
Swift4 + Moya + RxSwift + Codableで作るAPIクライアント

全体

APIClient.swift
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
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がプログラムの実行の全過程を通じて存続するようにしている。
APIClient.swift
final class APIClient {
    private init() {}
    static let shared = APIClient()
MultiTargetの使用
  • 異なる種類のTargetTypeをMoyaProviderに割り当てられるようにするため、MultiTargetを利用している。これにより、APIクライアント利用側ではジェネリクスを通じてMoyaProviderにTargetTypeを渡すという手間が省け、MoyaProviderの存在を利用側に対して隠蔽することができる。
APIClient.swift
private let provider = MoyaProvider<MultiTarget>()
Pluginの利用
  • 必要であれば、MoyaProviderを初期化する際、Pluginというものを渡す事ができる。これはリクエストを送る前、またはレスポンスを受け取った後などに何か副作用を記載したい場合に利用できる。用途としては、公式のコメントに記載があるように、リクエストを送る・レスポンスを受け取るときにログを出すであるとか、インジケータ(ローディング中である事を示すグルグル回るもの)の出し入れを行うとか、リクエストを送る前にURLRequestの設定を変更したい時などに利用できる。
APIClient.swift
private let provider = MoyaProvider<MultiTarget>(plugins: [FooPlugin()])
Plugin.swift
/// 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だと指定した秒数後にデータを返す事ができ、より本番に近い状況でテストしたい時には有効。
APIClient.swift
    private let stubbingProvider = MoyaProvider<MultiTarget>(stubClosure: MoyaProvider.immediatelyStub) //.delayed(seconds: 1.0)等でも可
レスポンスのチェック
  • 返ってきたレスポンスにはステータスコード・ヘッダーフィールド・データが含まれているが、これらに対して行いたい処理がプロジェクト全体で共通している場合は、APIクライアントに記載してしまうと良い。エラーハンドリングや、ログを出す等。
APIClient.swift
    // 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())
    }
HogeTargetType.swift
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() {}
}

HogeEntity.swift
struct HogeEntity: Codable {
   let fuga: String
Hoge.json
{
   "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