Third party framework read thousands of articles, the purpose of writing an article is to urge their complete study, another purpose is to output to force their own thinking

Moya is a web abstraction layer based on Alamofire. For a simple introduction, go to the Corresponding Github address

POP

Because the overall structure of the library is based on the idea of POP-oriented protocol programming, a few words about POP.

Protocol

A protocol is a definition of a set of properties and/or methods, and if a specific type wants to comply with a protocol, it needs to implement all of those things defined by that protocol. All a protocol really does is “a contract about implementation.”

In meow God’s blog, two apps about protocol are posted with the original address

Protocol-oriented Programming meets Cocoa (PART 1)

Protocol-oriented Programming meets Cocoa (Part 2)

TargetType

Is used to set up the protocol for requesting basic information.

  • baseURL
  • path
  • method
  • sampleData
  • task
  • validationType
  • header

Use enumerations to make it easier to manage. You can set the request information by clicking on the syntax in the request header. Class /struct can be used to achieve the effect of the group management API.

The sampleData attribute assigns custom test data to be used in conjunction with tests that request a return result

Where, the validationType attribute validates the results returned by Alamofire according to statusCode, which is the automatic verification function of Alamofire (no detailed in-depth study is made). See Alamofire automatic validation for this section

Simply implement a target

enum NormalRequest {
	case solution1
	case solution2
}

extension NormalRequest: TargetType {
	var baseURL: URL { return URL(String: "")! }
	var path: String { 
		switch self {
			case .solution1
				return ""
			default
				return ""}}.
	var headers: [String : String]? {
        return ["Content-type" : "application/x-www-form-urlencoded"]}}Copy the code

In the implementation, if a case corresponds to a specific APi, the actual amount of code is actually a lot of. So use case as grouping, and then add the associated value to the case.

case solution1(string)
Copy the code

In this way, different request methods corresponding to the same functional module can be managed uniformly, and network requests can be completed by adding new request addresses without modifying the request code. There can also be multiple associated values in a case, so from this point of view, the corresponding API will become more flexible.

There is also a MultiTarget in Moya, which is used to support merging multiple targets into one. The official sample

It also contains associated types, which use generics to model data after getting the request result, one target for one model.

MoyaProvider

MoyaProvider is the top-level request header of Moya, and the corresponding protocol is MoyaProviderType. After simply setting the target, initialize a provider corresponding to the target. No additional parameters are required

let provider = MoyaProvider<NormalRequest> ()Copy the code

But if you look directly at the init method, you’ll see that you can set seven parameters, all of which are given default implementations

public init(endpointClosure: @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping,
            requestClosure:  @escaping RequestClosure = MoyaProvider.defaultRequestMapping,
	          stubClosure:     @escaping StubClosure = MoyaProvider.neverStub,
            callbackQueue: DispatchQueue? = nil.session: Session = MoyaProvider<Target>.defaultAlamofireSession(),
            plugins: [PluginType] =[].trackInflights: Bool = false) {

        self.endpointClosure = endpointClosure
        self.requestClosure = requestClosure
        self.stubClosure = stubClosure
        self.session = sessio
        self.plugins = plugins
        self.trackInflights = trackInflights
        self.callbackQueue = callbackQueue
    }

Copy the code

In the MoyaProvider+Defaults file, there are only three methods, all of which are implemented through the Extension MoyaProvider extension. Init (endpointClosure, requestClosure, manager, etc.);

EndpointClosure

Here is the mapping from Target to Endpoint. The default implementation code is as follows

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

In the init method, we use an escape closure, so we return this function.

There are also two instance Endpoint methods that modify the request header data and parameter types, each returning a new Endpoint object

open func adding(newHTTPHeaderFields: [String: String]) -> Endpoint { . }
open func replacing(task: Task) -> Endpoint { . }
Copy the code

This is where the endpoint first appears to modify the request header and replace the parameters. That’s what it says in the official documentation

Note that we can rely on the existing behavior of Moya and extend-instead of replace — it. The adding(newHttpHeaderFields:) function allows you to rely on the existing Moya code and add your own custom values.

Note that we can rely on Moya’s existing behavior to extend it, not replace it. The add (newHttpHeaderFields 🙂 function enables you to rely on existing Moya code and add your own custom values.

let endpointClosure = { (target: MyTarget) - >Endpoint in
    let defaultEndpoint = MoyaProvider.defaultEndpointMapping(for: target)

    // Sign all non-authenticating requests
    switch target {
    case .authenticate:
        return defaultEndpoint
    default:
        return defaultEndpoint.adding(newHTTPHeaderFields: ["AUTHENTICATION_TOKEN": GlobalAppStorage.authToken])
    }
}
let provider = MoyaProvider<GitHub>(endpointClosure: endpointClosure)
Copy the code

It can be understood that these two modification methods are used when matching on the basis of the original modification.

RequestClosure

From Endpoint to URLRequest, an URLRequest is generated based on the Endpoint data. This closure serves as a transition

public typealias RequestClosure = (Endpoint.@escaping RequestResultClosure) - >Void
Copy the code

Generate an URLRequest. The default implementation is as follows

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)))}}Copy the code

Session

The default implementation here is an Alamofire.session object with a basic configuration.

For Star Quest Simmediately, it’s explained in the documentation

There is only one particular thing: since construct an Alamofire.Request in AF will fire the request immediately by default, even when “stubbing” the requests for unit testing. Therefore in Moya, startRequestsImmediately is set to false by default.

That is, every time a request is constructed, it is executed immediately by default. So set it to false in Moya

final class func defaultAlamofireSession() - >Session {
      let configuration = URLSessionConfiguration.default
      configuration.headers = .default
      return Session(configuration: configuration, startRequestsImmediately: false)}Copy the code

Plugins

Plugins, a very special feature of Moya. The corresponding protocol is PluginType

In the initialization method, the parameter type is an array of plug-ins that can be passed in multiple plug-ins at once. The method in the plug-in protocol is as follows:

/// 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>Copy the code

The timing of each method invocation

Stub

SampleData in target can provide its own test data that can be used to simulate the returned data results without using an actual network request. In provider initialization, the stubClosure closure is used to set whether mock tests are required

  • .never
  • .immediate
  • .delayed(seconds: TimeInterval)

There are more options for data testing in Moya, and sampleResponseClosure in the Endpoint can also be set up to simulate various errors

  • .networkResponse(Int, Data)
  • .response(HTTPURLResponse, Data)
  • .networkError(NSError)

Stub can set its own configuration in three places. The following uses the official example

// 1. Set test data
public var sampleData: Data {
    switch self {
    case .userRepositories(let name):
        return "[{\ \"name\\": \ \"Repo Name\ \"}]".data(using: String.Encoding.utf8)!}}// Whether and how to respond to the test data
let stubbingProvider = MoyaProvider<GitHub>(stubClosure: MoyaProvider.immediatelyStub)

// What response data is returned
let customEndpointClosure = { (target: APIService) - >Endpoint in
    return Endpoint(url: URL(target: target).absoluteString,
                    sampleResponseClosure: { .networkResponse(401 , /* data relevant to the auth error */) },
                    method: target.method,
                    task: target.task,
                    httpHeaderFields: target.headers)
}

let stubbingProvider = MoyaProvider<GitHub>(endpointClosure: customEndpointClosure, stubClosure: MoyaProvider.immediatelyStub)
Copy the code

Send the request

provider.request(.post("")){(result) in 
	swtich result {
	case let .success(response):
		break
	case let .fail(_) :break}}Copy the code

The request method in Moya is a unified request entry. Only need to configure the required parameters in the method, including the need to generate the request address, request parameters through enumeration type, very clear classification and management. Use the. Syntax to generate the corresponding enumeration, and then generate the endpoint, URLRequest, and so on.

In this method, you can modify the callbackQueue again (I haven’t seen the documentation on how to use the callbackQueue).

@discardableResult
    open func request(_ target: Target,
                      callbackQueue: DispatchQueue? = .none,
                      progress: ProgressBlock? = .none,
                      completion: @escaping Completion) -> Cancellable {

    let callbackQueue = callbackQueue ?? self.callbackQueue
    return requestNormal(target, callbackQueue: callbackQueue, progress: progress, completion: completion)
}
Copy the code

This method does some common processing for generating requests.

In the init method, you can not only pass in the corresponding parameters, but also have the purpose of holding those properties.

Initialize the closure required by the request

func requestNormal(_ target: Target, callbackQueue: DispatchQueue? , progress: Moya.ProgressBlock? , completion: @escaping Moya.Completion) -> Cancellable {}Copy the code

At the beginning of the method, three constants are defined

let endpoint = self.endpoint(target)
let stubBehavior = self.stubClosure(target)
let cancellableToken = CancellableWrapper()
Copy the code

Because init parameters are implemented by default, the first line of code generates the endpoint using the Moya library’s default implementation.

If it is not a custom implementation of endpointClosure, the defaultEndpointMapping method is called and an Endpoint object is returned

If a custom endpoint mapping closure is passed during initialization, it will be executed in a custom way

open func endpoint(_ token: Target) -> Endpoint {
      return endpointClosure(token)
}
Copy the code

CancellableToken is an object of class CancellableWrapper that follows the Cancellable protocol.

The protocol consists of just two lines of code, whether the property has been cancelled and the method to cancel the request. And what this method does is change the value of the property to true

/// A Boolean value stating whether a request is cancelled.
var isCancelled: Bool { get }

/// Cancels the represented request.
func cancel()
Copy the code

This is where the plug-in process is executed, once for each plug-in. All changes made to Result by the process methods implemented by the plug-in are eventually returned through Completion.

let pluginsWithCompletion: Moya.Completion = { result in
	let processedResult = self.plugins.reduce(result) { $1.process($0, target: target) }
	completion(processedResult)
}
Copy the code

Literally, this is the next step in actually executing the request. This closure is after the endpoint → URLRequest method has been executed

let performNetworking = { (requestResult: Result<URLRequest, MoyaError>) in // Is return the wrong type for the cancel the error data of the if cancellableToken. IsCancelled {self. CancelCompletion (pluginsWithCompletion, target: target) return } var request: URLRequest! switch requestResult { case .success(let urlRequest): request = urlRequest case .failure(let error): PluginsWithCompletion (.failure(error)) return} // Allow plugins to modify request  self.plugins.reduce(request) { $1.prepare($0, target: Target)} // Define the return Result closure, which returns the data returned by the request mapped to Result let networkCompletion: Moya.Completion = { result in if self.trackInflights { .... PluginsWithCompletion (result)}} This step is the next step in executing the request. Will continue to pass all the parameters cancellableToken. InnerCancellable = self. PerformRequest (target, request: preparedRequest, callbackQueue: callbackQueue, progress: progress, completion: networkCompletion, endpoint: endpoint, stubBehavior: stubBehavior) }Copy the code

The next step is to pass the two closures defined above into the requestClosure closure

The endpoint generates the URLRequest, and then executes the code in the performNetworking closure once the request is complete

requestClosure(endpoint, performNetworking)

Copy the code

In this method, there are two pieces of code based on the trackInflights property.

After looking at some data, I find that there is very little mention of the interpretation of this attribute, which can be seen from the code logic whether this is the handling of repeated requests. One explanation is whether to track repeated network requests

In #229, the mention of this feature refers to Moya initially tracking API requests and then doing something to prevent repeated requests. There were times when we needed to request an API repeatedly, so #232 removed the code that prevented repeated requests. In #477, the current version uses dictionary management for repeated API requests.

if trackInflights { objc_sync_enter(self) var inflightCompletionBlocks = self.inflightRequests[endpoint] inflightCompletionBlocks? .append(pluginsWithCompletion) self.inflightRequests[endpoint] = inflightCompletionBlocks objc_sync_exit(self) if inflightCompletionBlocks ! Return cancellableToken} else {// If there is no value where key is set to the endpoint, Initialize an objc_sync_Enter (self) self.inflightrequests [endpoint] = [pluginsWithCompletion] objc_sync_exit(self)}}.... let networkCompletion: Moya.Completion = { result in if self.trackInflights { self.inflightRequests[endpoint]? .forEach { $0(result) } objc_sync_enter(self) self.inflightRequests.removeValue(forKey: endpoint) objc_sync_exit(self) } else { pluginsWithCompletion(result) } }Copy the code

A request with trackInflights set to true when init will store the endpoint of the request in Moya. When the data is returned, if a duplicate request needs to be traced, the data returned by the request is actually sent once and returned multiple times.

Next pass parameters

cancellableToken.innerCancellable = self.performRequest(target, request: PreparedRequest: callbackQueue: callbackQueue, progress: progress, completion: NetworkCompletion, // After the network request succeeds, return the result to the closure endpoint: stubBehavior: endpoint)Copy the code

The internal implementation of this method executes the corresponding request mode according to switch stubBehavior and endpoint.task respectively. Only the simplest one is posted here

switch stubBehavior {
 case .never:
  switch endpoint.task {
    case .requestPlain, .requestData, .requestJSONEncodable, .requestCustomJSONEncodable, .requestParameters, .requestCompositeData, .requestCompositeParameters:
         return self.sendRequest(target, request: request, callbackQueue: callbackQueue, progress: progress, completion: completion)
	  ....
	}
	default:
		return self.stubRequest...
}

Copy the code

Implementation of a general request. This layer is the one associated with Alamofire. Because we’ve already generated urlRequest before. Therefore, in the previous step, the method of the corresponding Alamofire request header will be called and DataRequest, DownloadRequest and UploadRequest will be generated accordingly

func sendRequest(_ target: Target, request: URLRequest, callbackQueue: DispatchQueue? , progress: Moya.ProgressBlock? , completion: @escaping Moya.Completion) -> CancellableToken { let initialRequest = manager.request(request as URLRequestConvertible) // Target's special attributes, as mentioned at the beginning of the article, Here only USES the let validationCodes = target. ValidationType. StatusCodes let alamoRequest = validationCodes. IsEmpty? initialRequest : initialRequest.validate(statusCode: validationCodes) return sendAlamofireRequest(alamoRequest, target: target, callbackQueue: callbackQueue, progress: progress, completion: completion) }Copy the code

Next up is Alamofire.

The actual request

There are four different request methods

  • sendUploadMultipart
  • sendUploadFile
  • sendDownloadRequest
  • sendRequest

Each of these methods ends up calling the generic sendAlamofireRequest method

func sendAlamofireRequest<T>(_ alamoRequest: T, target: Target, callbackQueue: DispatchQueue? , progress progressCompletion: Moya.ProgressBlock? , completion: @escaping Moya.Completion) -> CancellableToken where T: Requestable, T: Request { }Copy the code

That’s what this method does

  1. Plugin notifies willsend
let plugins = self.plugins
plugins.forEach { $0.willSend(alamoRequest, target: target) }

Copy the code
  1. Initialize the Progress closure
var progressAlamoRequest = alamoRequest let progressClosure: (Progress) -> Void = { progress in let sendProgress: () -> Void = { progressCompletion? (ProgressResponse(progress: progress)) } if let callbackQueue = callbackQueue { callbackQueue.async(execute: } else {sendProgress()}} // Convert to the corresponding request header if progressCompletion! = nil {switch progressAlamoRequest {···}}Copy the code
  1. Encapsulate request results
Let result = convertResponseToResult(response, request: request, data: data, error: Public func convertResponseToResult(_ Response: HTTPURLResponse? , request: URLRequest? , data: Data? , error: Swift.Error?) -> Result<Moya.Response, MoyaError> { }Copy the code
  1. Returns the resulting closure, including the progress of the didReceive transfer for the sending plug-in
let completionHandler: RequestableCompletion = { response, request, data, error in
    let result = convertResponseToResult(response, request: request, data: data, error: error)
    // Inform all plugins about the response
    plugins.forEach { $0.didReceive(result, target: target) }
    if let progressCompletion = progressCompletion {
        switch progressAlamoRequest {
        case let downloadRequest as DownloadRequest:
            progressCompletion(ProgressResponse(progress: downloadRequest.progress, response: result.value))
        case let uploadRequest as UploadRequest:
            progressCompletion(ProgressResponse(progress: uploadRequest.uploadProgress, response: result.value))
        case let dataRequest as DataRequest:
            progressCompletion(ProgressResponse(progress: dataRequest.progress, response: result.value))
        default:
            progressCompletion(ProgressResponse(response: result.value))
        }
    }
    completion(result)
}

Copy the code
  1. Execute the request.
progressAlamoRequest = progressAlamoRequest.response(callbackQueue: callbackQueue, completionHandler: CompletionHandler) progressAlamoRequest. Resume () / / return is a CancellableToken object return CancellableToken (request: progressAlamoRequest)Copy the code

Completion

In the request return Result, the success or failure of the request is judged by Result

public typealias Completion = (_ result: Result<Moya.Response, MoyaError>) -> Void

Copy the code

MoyaError is a custom enumeration of Error types inherited from swift.error

public enum MoyaError: Swift.Error { }

Copy the code

When a request fails, the response status code is obtained by returning a response

let code = (error as! MoyaError).response? .statusCodeCopy the code

Other wrong situations

// cancelCompletion method let error = moyaerror. dredrel (NSError(domain: NSURLErrorDomain, code: NSURLErrorCancelled, userInfo: nil), Nil) // defaultRequestMapping failed to generate URLRequest 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)))} // convertResponseToResult Encapsulates the Result returned by the request when Result fails switch (response, data, error) {... case let (.some(response), _, .some(error)): let response = Moya.Response(statusCode: response.statusCode, data: data ?? Data(), request: request, response: response) let error = MoyaError.underlying(error, response) return .failure(error) ...Copy the code

The lock

What defer {} does in Swift is that the block declared by defer is called after the current code (called when the current scope exits) exits

NSRecursiveLock can be fetched multiple times from the same thread without causing deadlock problems.

private let lock: NSRecursiveLock = NSRecursiveLock(a)var willSend: ((URLRequest) - >Void)? {
        get {
            lock.lock(); defer { lock.unlock() }
            return internalWillSend
        }

        set {
            lock.lock(); defer { lock.unlock() }
            internalWillSend = newValue
        }
    }
Copy the code
fileprivate var lock: DispatchSemaphore = DispatchSemaphore(value: 1)

    public func cancel(a) {
        _ = lock.wait(timeout: DispatchTime.distantFuture)
        defer { lock.signal() }
        guard !isCancelled else { return }
        isCancelled = true
    }
Copy the code

On protocol oriented design architecture

PluginType and TargetType are the most commonly used protocols in development

The plug-in protocol is simple to understand, and the call timing is set for the requested method. Add a plug-in when the method is requested, and the implementation of each plug-in’s timing is executed in turn during the request. This is where protocol oriented programming comes in. We call the protocol’s methods in business code, not as a concrete object. Any object that implements a protocol only needs to focus on the implementation of the protocol method.

Replace Alamofire?

Moya works as a network abstraction and is protocol oriented. So from the implementation point of view, if the bottom layer to change a network request implementation library, it should be basically no impact on the previous code. If you need to replace the underlying implementation library, what changes would you need to make?

So where does the link between Moya and Alamofire appear? The connection between Moya and Alamofire is mainly concentrated in the file Moya+Alamofire. Swift. If a replacement is required, it is almost as simple as replacing the Alamofire-related attributes in the alias to complete the replacement of the underlying request.

Refer to the link

How to use Moya more deeply

Moya’s approach to design