An example of Moya + RxSwift API client (Swift5)

Premise

By default, Swift uses the URLSession class for communication processing, but Moya is a library that serves as a wrapper for it.

Since it can be written conveniently, it seems to be a standard method at the development site. Especially convenient when combined with RxSwift.

For the time being, it is better to know the URLSession class to some extent, so beginners should first get used to URLSession and then move to Moya and realize that it can be used conveniently.

Official API client made with Swift4 + Moya + RxSwift + Codable

The entire

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.Reponse> {
        Single<G.Reponse>.create { observer in
            self.makeRequest(request)
                .subscribe(onSuccess: { response in
                    observer(.success(response))
                }, onError: { error in
                    //Error handling, etc. that you want to do in common throughout the project
                    observer(.error(error))
                })
        }
    }

    func makeRequest<G: ApiTargetType>(_ request: G) -> Single<G.Reponse> {
        provider.rx
            .request(MultiTarget(request))
            .flatMap({ response -> Single<Response> in
                //Checking the response header
                return Single.just(try response.lookingAllHeaderFields())
            })
            .flatMap { response -> Single<Response> in
                //Error code check
                return Single.just(try response.successfulStatusCodesPolicy())
        }
        .map(G.Reponse.self, failsOnEmptyData: false)
    }

    func requestStub<G: ApiTargetType>(_ request: G) -> Single<G.Reponse> {
        Single<G.Reponse>.create { observer in
            self.makeRequestStub(request)
                .subscribe(onSuccess: { response in
                    observer(.success(response))
                }, onError: { error in
                    if let error = error as? MoyaError {
                    //Error handling, etc. that you want to do in common throughout the project
                    observer(.error(error))
                })
        }
    }

    func makeRequestStub<G: ApiTargetType>(_ request: G) -> Single<G.Reponse> {
        stubProvider.rx
            .request(MultiTarget(request))
            .flatMap({ response -> Single<Response> in
                //Checking the response header
                return Single.just(try response.lookingAllHeaderFields())
            })
            .flatMap { response -> Single<Response> in
                //Error code check
                return Single.just(try response.successfulStatusCodesPolicy())
        }
        .map(G.Reponse.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)")!
    }
}

Commentary

API client is a singleton object

--Since API communication is performed, it is necessary to avoid the situation where the object of MoyaProvider class is released during communication. On the side that uses the API client, it is also possible to retain the object of the MoyaProvider class for a sufficiently long period (such as setting it as a member variable). Here, to avoid such trouble, set shared as Singleton object so that MoyaProvider will survive throughout the execution of the program. ing.

APIClient.swift


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

--- MultiTarget is used so that different types of TargetType can be assigned to MoyaProvider. As a result, the API client user side can save the trouble of passing the TargetType to the MoyaProvider through generics, and can hide the existence of the MoyaProvider from the user side.

APIClient.swift


private let provider = MoyaProvider<MultiTarget>()
Use of Plugin

--If necessary, you can pass a Plugin when initializing MoyaProvider. This can be used if you want to describe any side effects, such as before sending a request or after receiving a response. As stated in the official comment, it is used for sending a request, logging when receiving a response, putting in and out an indicator (a thing that turns around indicating that it is loading), etc. It can be used when you want to change the URL Request setting before sending.

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>
}
Use of stubProvider

--If you use stubProvider, you can return the sample data described in each TargetType as a response. As for the behavior of stubs, .immediatelyStub returns stub data immediately, but .delay can return data after the specified number of seconds, which is effective when you want to test in a situation closer to production.

APIClient.swift


    private let stubbingProvider = MoyaProvider<MultiTarget>(stubClosure: MoyaProvider.immediatelyStub) //.delayed(seconds: 1.0)Etc. are also possible
Checking the response

--The returned response contains status code, header field, and data, but if the processing you want to perform for these is common to the entire project, it is better to describe it in the API client. Error handling, logging, etc.

APIClient.swift


    // MARK: - Public

    func request<G: ApiTargetType>(_ request: G) -> Single<G.Reponse> {
        Single<G.Reponse>.create { observer in
            self.makeRequest(request)
                .subscribe(onSuccess: { response in
                    observer(.success(response))
                }, onError: { error in
                    //Error handling, etc. that you want to do in common throughout the project
                    observer(.error(error))
                })
        }
    }

    func makeRequest<G: ApiTargetType>(_ request: G) -> Single<G.Reponse> {
        provider.rx
            .request(MultiTarget(request))
            .flatMap({ response -> Single<Response> in
                //Checking the response header
                return Single.just(try response.lookingAllHeaderFields())
            })
            .flatMap { response -> Single<Response> in
                //Error code check
                return Single.just(try response.successfulStatusCodesPolicy())
        }
        .map(G.Reponse.self, failsOnEmptyData: false)
    }

Target type

If there is something that is common to the entire project (typically, the base URL, etc.), it is a good idea to put it out and create a TargetType protocol.

python


protocol ApiTargetType: TargetType {
    associatedtype Response: Codable
}

extension ApiTargetType {

    /// The target's base `URL`.
    var baseURL: URL {
        URL(string: "\(URLs.baseURL)")!
    }
}

Usage example

python


    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"
}

Reference material

API client made with Swift4 + Moya + RxSwift + Codable Moya/Examples/Multi-Target/ViewController.swift

Recommended Posts

An example of Moya + RxSwift API client (Swift5)
[swift5] Try to make an API client with various methods
Find an approximation of sinx using Swift
Find an approximation of cosx in Swift
A simple example of an MVC model
I made an API client for Nature Remo