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
- Plugin notifies willsend
let plugins = self.plugins
plugins.forEach { $0.willSend(alamoRequest, target: target) }
Copy the code
- 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
- 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
- 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
- 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