Moya introduction

Moya is a set of network abstraction layer framework based on Alamofire.

Personally, I think Alamofire is based on how to call requests more conveniently in URLSession, while Moya is based on Alamofire to manage apis better by abstracting URLs, parameters, etc.

The basic template

Moya’s encapsulation of the API is based on enum, generating requests through different uses of enumeration for different endpoints.

enum GitHub {
    case zen
    case userProfile(String)}extension GitHub: TargetType {
    var baseURL: URL { return URL(string: "https://api.github.com")! }
    var path: String {
        switch self {
        case .zen:
            return "/zen"
        case .userProfile(let name):
            return "/users/\(name)"}}var method: Moya.Method {
        return .get
    }

    var task: Task {
        return .requestPlain
    }

    var sampleData: Data {
        switch self {
        case .zen:
            return "Half measures are as bad as nothing at all.".data(using: String.Encoding.utf8)!
        case .userProfile(let name):
            return "{\"login\": \"\(name)\", \"id\": 100}".data(using: String.Encoding.utf8)! }}var validationType: ValidationType {
        return .successAndRedirectCodes
    }

    var headers: [String: String]? {
        return nil}}Copy the code

Inherit TargetType by enumeration, adding a detailed implementation.

var provider = MoyaProvider<GitHub>()
provider.request(target) { response in
    if case .failure(let error) = response {
        receivedError = error
    }
}
Copy the code

Finally, the generate generates the request based on the TargetType provider.

This is the basic implementation of Moya. It is too basic to repeat.

Codable

Codable is Apple’s protocol for parsing data into Models without using third-party libraries such as ObjectMapper and SwiftyJson.

Here’s a simple Codable example:

struct Demo: Codable {
    var name: String
    var age: Int
}

func decode(a) {
    let jsonString =  "{\"name\":\"zhangsan\", \"age\":15}" // Simulate JSON data
    let decoder = JSONDecoder(a)let data = jsonString.data(using: .utf8)!
    let model = try! decoder.decode(Demo.self, from: data)
    print(model) // Demo(name: "zhangsan", age: 15)
}
Copy the code

The corresponding processing has been encapsulated in Moya’s Response

 DemoProvider.provider.request(.zen) { (result) in
    switch result {
    case .success(let response):
        if let model = try? response.map(Demo.self) {
            success(model)
        }
    case .failure(let error):
        break}}Copy the code

If the data is in several levels of JSON, it can also be retrieved by setting keypath:

{
    data: {
        name: "test",
        age: 15}}try? response.map(Demo.self, atKeyPath: "data")

Copy the code

Note that the function also has an argument called failsOnEmptyData, which is set to true by default. If the data returned is empty, parsing will fail.

EndPoint

The EndPoint is half of Moya’s internal data structure, generated by the TargetType used, which is ultimately used to generate network requests. Each EndPoint stores the following data:

/// A string representation of the URL for the request.
public let url: String

/// A Closure Responsibility for Returning an 'EndpointSampleResponse'. (unit testing)
public let sampleResponseClosure: SampleResponseClosure

/// The HTTP method for the request.
public let method: Moya.Method

/// The `Task` for the request.
public let task: Task

/// The HTTP header fields for the request.
public let httpHeaderFields: [String: String]?
Copy the code

When the Provider is generated, you can pass in an endpointClosure to customize how the TargetType is delivered to the Endpoint.

Default implementation:

final class func defaultEndpointMapping(for target: Target) - >Endpoint {
    return Endpoint(
        url: URL(target: target).absoluteString,
        sampleResponseClosure: { .networkResponse(200, target.sampleData) },
        method: target.method,
        task: target.task,
        httpHeaderFields: target.headers
    )
}
Copy the code

Here you can redefine how the Endpoint is generated, for example:

// Change all generated Endpoint requests to get
let endpointClosure = { (target: MyTarget) - >Endpoint in
    let url = URL(target: target).absoluteString
    return Endpoint(url: url, sampleResponseClosure: {.networkResponse(200, target.sampleData)}, method: .get, task: target.task)
}
Copy the code

Or modify the generated Endpoint:

let endpointClosure = { (target: MyTarget) - >Endpoint in
    let defaultEndpoint = MoyaProvider.defaultEndpointMapping(for: target)
    return defaultEndpoint.adding(newHTTPHeaderFields: ["APP_NAME": "MY_AWESOME_APP"])}Copy the code

Note: If you modify an already initialized Endpoint directly, you can only modify task and add headers.

Request

After an Endpoint is generated, it is converted to URLRequst for use.

The default implementation of Moya:

RequestResultClosure = (Result<URLRequest.MoyaError- > >)Void

final class func defaultRequestMapping(for endpoint: Endpoint.closure: RequestResultClosure) {
    do {
     let urlRequest = try endpoint.urlRequest()
        closure(.success(urlRequest))
    } catch MoyaError.requestMapping(let url) {
        closure(.failure(MoyaError.requestMapping(url)))
    } catch MoyaError.parameterEncoding(let error) {
        closure(.failure(MoyaError.parameterEncoding(error)))
    } catch {
        closure(.failure(MoyaError.underlying(error, nil)))}}public func urlRequest(a) throws -> URLRequest {
        guard let requestURL = Foundation.URL(string: url) else {
            throw MoyaError.requestMapping(url)
        }

        var request = URLRequest(url: requestURL)
        request.httpMethod = method.rawValue
        request.allHTTPHeaderFields = httpHeaderFields

        switch task {
        case .requestPlain, .uploadFile, .uploadMultipart, .downloadDestination:
            return request
        case .requestData(let data):
            request.httpBody = data
            return request
......
Copy the code

Because inside have to realize how to generate the Request, most don’t need to modify the urlRequest, but redefine requestClosure, modify has generated good Request and below is directly modify the Request cache strategy, and error handling:

let requestClosure = { (endpoint: Endpoint, done: MoyaProvider.RequestResultClosure) in
    do {
        var request = try endpoint.urlRequest()
        request.cachePolicy = .reloadIgnoringCacheData
        done(.success(request))
    } catch {
        done(.failure(MoyaError.underlying(error)))
    }

}
Copy the code

stubClosure

StubClosure implementation:

 /// Do not stub.
final class func neverStub(_: Target) - >Moya.StubBehavior {
    return .never
}

/// Return a response immediately.
final class func immediatelyStub(_: Target) - >Moya.StubBehavior {
    return .immediate
}

/// Return a response after a delay.
final class func delayedStub(_ seconds: TimeInterval) - > (Target) - >Moya.StubBehavior {
    return { _ in return .delayed(seconds: seconds) }
}
Copy the code

The default implementation of Moya is neverStub. When the immediatelyStub or delayedStub is used, the request to the network will not take the real data, but will return the data of SimpleData in Target, which is generally used to test the processing of data returned by the API.

DelayedStub specifies the delay time relative to the immediatelyStub, in seconds.

callbackQueue

You can specify the callback thread after the network request is returned. By default all requests will be put into the background thread by Alamofire and CallBAC will be called in the main thread.

Manager

public typealias Manager = Alamofire.SessionManager

final class func defaultAlamofireManager() - >Manager {
    let configuration = URLSessionConfiguration.default
    configuration.httpAdditionalHeaders = Manager.defaultHTTPHeaders

    let manager = Manager(configuration: configuration)
    manager.startRequestsImmediately = false
    return manager
}
Copy the code

The Manager used in Moya is actually Alamofire’s Manager.

You can set a Timeout, cache policy, and so on

let manager: SessionManager = {
    let configuration = defaultURLSessionConfiguration
    configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
    configuration.timeoutIntervalForRequest = 20
    let trustPolicyManager = ServerTrustPolicyManager(policies:
        [
            "www.baidu.com": ServerTrustPolicy.disableEvaluation
        ]
    )
    let manager = SessionManager(configuration: configuration, serverTrustPolicyManager: trustPolicyManager)
    return manager
}()
Copy the code

Plugins

Plugins are plugins that follow the PluginType. A single provider can be used for multiple plugins.

PluginType:

public protocol PluginType {
    /// There is still a chance to modify the request before sending it
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest

    /// called before sending
    func willSend(_ request: RequestType, target: TargetType)

    /// After the Response is received and before the callback is triggered
    func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType)

    /// Result can also be modified before Callback is called
    func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response.MoyaError>}public extension PluginType {
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest { return request }
    func willSend(_ request: RequestType, target: TargetType){}func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType){}func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response.MoyaError> { return result }
}
Copy the code

There are many things you can do in the Plugin

  • Logging network Requests
  • Handles hiding or showing network Activity Progress
  • Do more processing with the request

Such as:

struct TestPlugin: PluginType {
    // Do more processing on the request
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
        var request = request
        if target is GitHub {
            request.timeoutInterval = 5
        }
        return request
    }
    
    // Log the network request
    func willSend(_ request: RequestType, target: TargetType) {
        print("start")
        print(request.request? .url ??"")}// Log the network request
    func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
        print("end")
        switch result {
        case .success(let response):
            print("end success")
            print(response.request? .url ??"")
        case .failure(let error):
            print("end failure")
            print(error)
        }
    }
    
    // Modify the result returned
    func process(_ result: Result<Response, MoyaError>, target: TargetType) -> Result<Response.MoyaError> {
        if case let .failure(error) = result {
            return .failure(MoyaError.underlying(error, nil))}return result
    }
}
Copy the code

Moya also provides plugins for Logger, activity, etc., which are implemented by default.

trackInflights

Read the source code for a long time or do not understand, hope to understand a friend can tell me how to use.

MultiTarget

Typically, a targetType corresponds to a Provider

let githubProvider = MoyaProvider<GitHub>(stubClosure: MoyaProvider.immediatelyStub, trackInflights: true)
let demoProvider = MoyaProvider<Demo>(stubClosure: MoyaProvider.immediatelyStub, trackInflights: true)
Copy the code

But if you want to make the Provider more generic, you can write:

let commonProvider = MoyaProvider<MultiTarget> ()Copy the code

When called, specify TargetType:

commonProvider.request(MultiTarget(GitHub.zen)) { result in. }Copy the code

process

Make up the flow chart I found online