1, the preface
It’s a shame to say so.
Swift has only recently been used for projects, and as a friend of mine scoffs: “We were using shit before you started using it, and I was speechless.
That since use Swift, want to find a way to use comfortable, use clear. From OC project to Swift project, some OC libraries, such as network Request library (AFNetworking), Json parsing (YYModel), responsive programming (RAC), and network request encapsulation library (self-encapsulated or third-party) will need to be changed as needed.
2. The selection of the third library
1. Network request library
Alamofire, of course, just as OC chose AFNetworking.
2. Json parsing
Swift also has many, such as SwiftyJSON, HandyJSON and so on.
SwiftyJSON is a very powerful tool for converting Json to a dictionary, and also for path nulling when fetching by key, but I personally find it a bit strange to use.
Later, I chose HandyJSON, HandyJSON also supports structure, support Json into objects, support model array, because of the support for generics on Swift, so compared with YYModel on OC to use more comfortable.
Responsive programming
Swift is a static language that uses chained function programming, while responsive programming in Swift makes Swift simpler and lighter.
At present, there are many options, such as ReactiveCocoa(Swift), RxSwift and Swift Combine(Apple’s own), each of which has its own advantages and disadvantages. Each guest officer can compare and choose freely. If you contact with each other for the first time, you can choose one at will (after all, only after using it can you compare).
-
RxSwift has a lot of maintenance staff, which means you can easily find a solution to a problem, and RxSwift is just one of the ReactiveX, it also has RxJava, RxPython, etc. Learn one, maybe all the others are the same.
-
ReactiveCocoa(Swift), which is translated from OC, has some historical OC baggage, but those familiar with RAC will be easier to get started.
-
The Swift Combine is Apple’s own, own son, and will be updated more often in the future, and there will be no third library not being maintained and updated.
4. Network library encapsulation
If your company’s OC project has a good, powerful library repackaged in the network library, then you don’t need to look at this, you will have to mix.
Moya is recommended for those who have simply repackaged AFNetworking or are new projects.
Moya is just a reencapsulation of Alamofire, not a network request library, so using Moya requires using Alamofire.
Since this is a repackaging of the network library, we can replace Alamofire with something else, just rewrite Moya+Alamofire. Swift. Personally, I don’t think it’s necessary.
3. Use method
Moya is the re-encapsulation of Alamofire. If you just use Moya, you can just care about the use method of Moya.
Moya provides Moya English documents and Moya Chinese documents respectively. (English version is more comprehensive)
1. Familiar with Moya
After downloading the official Demo, familiarize yourself with Moya.
- Moya Usage Official English document
- Moya Usage official Chinese document
The documentation is already very detailed, so here is a brief explanation
/// create a file myservice.swift
/// Declare an enumeration
enum MyService {
/// categorize your request call function
case createUser(firstName: String, lastName: String)
}
/// extend your enumeration to comply with the TargetType protocol
extension MyService: TargetType {
var baseURL: {
/ / / in the host
return baseURL;
}
var path: String {
case createUser(let firstName, let lastName)
// return the request path
return "/user/create/user"
}
var method: Moya.Method {
switch self {
case .createUser:
/// returns.get or.post
return .post;
}
}
var task: Task {
switch self {
case .createUser(let firstName, let lastName):
/// Specify the request parameters
return .requestParameters(parameters: ["first_name": firstName, "last_name": lastName], encoding: JSONEncoding.default)
}
}
var sampleData: Data {
/// If the server gives you a test example, you can put it here
case .createUser(let firstName, let lastName):
return "{\"id\": 100, \"first_name\": \"\(firstName)\", \"last_name\": \"\(lastName)\"}".utf8Encoded
}
var headers: [String: String]? {
/// request header Settings
return ["Content-type": "application/json"]}}Copy the code
Then you can call it in your ViewController:
let provider = MoyaProvider<MyService>()
provider.request(.createUser(firstName: "James", lastName: "Potter")) { result in
// do something with the result (read on for more details)
}
// The full request will result to the following:
// POST https://api.myservice.com/users
// Request body:
/ / {
// "first_name": "James",
// "last_name": "Potter"
// }
Copy the code
2. Learn about Moya
The above is just a preliminary use of Moya, but the specific business is much more complex than Demo, Moya also provides us with quite sufficient space to display.
The first step is again to create a file declaring an enumeration that implements the TargetType protocol. But creating a MoyaProvider object is different.
Let provider = MoyaProvider
(); To be specific:
/// Initializes a provider.
public init(endpointClosure: @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping,
requestClosure: @escaping RequestClosure = MoyaProvider.defaultRequestMapping,
stubClosure: @escaping StubClosure = MoyaProvider.neverStub,
callbackQueue: DispatchQueue? = nil,
manager: Manager = MoyaProvider<Target>.defaultAlamofireManager(),
plugins: [PluginType] = [],
trackInflights: Bool = false) {
self.endpointClosure = endpointClosure
self.requestClosure = requestClosure
self.stubClosure = stubClosure
self.manager = manager
self.plugins = plugins
self.trackInflights = trackInflights
self.callbackQueue = callbackQueue
}
/// Returns an `Endpoint` based on the token, method, and parameters by invoking the `endpointClosure`.
open func endpoint(_ token: Target) -> Endpoint {
return endpointClosure(token)
}
Copy the code
Here you can see that the MoyaProvider object init provides seven additional parameters, but if you use the default init, the others are automatically assigned default values.
1, endpointClosure
The default source 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
Here we convert the created protocol compliance enumeration MyService to an Endpoint, often just using its default method. When you look at the Endpoint, there are two other methods:
-
Open func Adding (newHTTPHeaderFields: [String: String]) -> Endpoint: Used to change the request header.
-
Open Func Replacing (Tasks: Tasks) -> Endpoint: Replacing the tasks implemented in the MyService enumeration.
But sometimes there are business testing requirements, such as network errors, timeouts, etc. I can do it right here.
Since it is a closure, it will be executed every time the API is called, so you can do whatever you want.
Moya gives an example of simply passing failureEndpointClosure to the MoyaProvider parameter endpointClosure.
let failureEndpointClosure = { (target: MyService) -> Endpoint in
let sampleResponseClosure = { () -> (EndpointSampleResponse) in
if shouldTimeout {
return .networkError(NSError())
} else {
return .networkResponse(200, target.sampleData)
}
}
return Endpoint(url: URL(target: target).absoluteString,
sampleResponseClosure: sampleResponseClosure,
method: target.method,
task: target.task)
}
Copy the code
Here you can convert MyService into an Endpoint object and arbitrarily change the parameters to meet various testing requirements.
2, requestClosure
Generate URLRequest based on the Endpoint.
The default source 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
UrlRequest (let urlRequest = try endpoint.urlRequest()) For example, you need to set timeoutInterval for URLRequest.
The following is an example:
let requestClosure = { (endpoint: Endpoint, done: MoyaProvider.RequestResultClosure) in
do {
var request: URLRequest = try endpoint.urlRequest()
request.httpShouldHandleCookies = false
request.timeoutInterval = 15
done(.success(request))
} catch {
done(.failure(MoyaError.underlying(error, nil)))
}
}
Copy the code
3, stubClosure
This parameter provides three enumerations:
-
.never (default) : requests the server directly;
-
. Immediate: sampleData example data in the protocol.
-
.delayed(seconds) Can delay stub requests for a specified amount of time, for example,.delayed(0.2) can delay each stub request for 0.2s. This is useful for simulating network requests in unit tests.
Official example:
let stubClosure = { target: MyService -> Moya.StubBehavior in
switch target {
/* Return something different based on the target. */}}Copy the code
4, callbackQueue
Callback thread.
5, manager
Here directly use the official explanation, most projects here are used by default.
Next comes the session parameter, which by default gets a custom alamofire. session instance object initialized with basic configuration
final class func defaultAlamofireSession(a) -> Session {
let configuration = URLSessionConfiguration.default
configuration.headers = .default
return Session(configuration: configuration, startRequestsImmediately: false)}Copy the code
There is only one thing to note here: since creating an Alamofire.Request object in AF triggers the Request immediately by default, even “stubbing” requests for unit tests are the same. So in Moya, the startRequestsImmediately property is set to false by default.
If you need to customize your session, for example by creating an SSL pinning and adding it to the session, all requests will be routed through the custom configured session.
let serverTrustManager = ServerTrustManager(evaluators: ["example.com": PinnedCertificatesTrustEvaluator()])
let session = Session(
configuration: configuration,
startRequestsImmediately: false,
serverTrustManager: serverTrustManager
)
let provider = MoyaProvider<MyTarget>(session: session)
Copy the code
6, the plugins
Plugins are an array of interceptors that can pass in multiple objects that conform to the PluginType protocol. Refer to the PluginType protocol:
/// - inject additional information into a request
public protocol PluginType {
/// Called to modify a request before sending.
// Call this method back after requestClosure generates URLRequest
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest
/// Called immediately before a request is sent over the network (or stubbed).
// Call back this method before the network request is issued
func willSend(_ request: RequestType, target: TargetType)
/// Called after a response has been received, but before the MoyaProvider has invoked its completion handler.
/// This method is called back when Moya has not processed the data received
func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType)
/// Called to modify a result before completion.
CallBack this method before the network callBack closure callBack
func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError>
}
Copy the code
There’s so much going on here.
- Such as:
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest
Method can be used to concatenate public parameters (version number, token, userID) or dataRSA
Encryption is signed.
An 🌰 :
/// Called to modify a request before sending.
public func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
/// public parameters here
let target = target as! MyService
var parameters : [String: Any]?
if let requstData = request.httpBody {
do {
let json = try JSONSerialization.jsonObject(with: requstData, options: .mutableContainers)
parameters = json as? [String: Any]
} catch {
/// Failed to process...}}else {
parameters = [String: Any]()
}
/// Concatenate public parameters
parameters = paramsForPublicParmeters(parameters: parameters)
/// encryption is signed
parameters = RSA.sign(withParamDic: parameters)
do {
/ / / replace httpBody
if let parameters = parameters {
return try request.encoded(parameters: parameters, parameterEncoding: JSONEncoding.default)}}catch {
/// Failed to process...
}
return request
}
Copy the code
- Such as:
func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError>
After the method is called back, the data can be checked and decrypted.
An 🌰 :
/// Called to modify a result before completion.
public func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError> {
/ / / attestation
if case .success(let response) = result {
do {
let responseString = try response.mapJSON()
/// Json to a dictionary
let dic = JsonToDic(responseString)
/ / / attestation
if let _ = SignUntil.verifySign(withParamDic: dic) {
// decrypt data
dic = RSA.decodeRSA(withParamDic: dic)
/// regenerate moya.response
/ / /...
/ / / return Moya. The response
return .success(response)
} else {
let error = NSError(domain: "Failed in visa inspection", code: 1, userInfo: nil)
return .failure(MoyaError.underlying(error, nil))
}
} catch {
let error = NSError(domain: "Interceptor response to JSON failed", code: 1, userInfo: nil)
return .failure(MoyaError.underlying(error, nil))
}
} else {
/// return the original failed
return result
}
}
Copy the code
- You can still be there
willSend
和didReceive
Do log printing:
An 🌰 :
/// Intercepts print logs before sending
public func willSend(_ request: RequestType, target: TargetType) {
// request log printingNetWorkingLoggerOutPut.outPutLoggerRequest(request.request, andRequestURL: request.request? .url? .absoluteString) }/// intercept the print log when it is about to be received
public func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) {
/// Returns log printing
switch result {
case .success(let response):
NetWorkingLoggerOutPut.outPutLoggerReponseString(response.response, andRequest: response.request, andResponseObj:tryResponseToJSON(response: response) )
case.failure(let error): NetWorkingLoggerOutPut.outPutLoggerReponseString(error.response? .response, andRequest: error.response? .request, andResponseObj: tryResponseToJSON(response: error.response)) } }Copy the code
Sure, these are just snippets of code, but the important ones have been posted so you can use them as inspiration for further expansion.
7, trackInflights
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.
3. Use Moya
3.1 and 3.2 basically specify the use of Moya, so let’s talk about how to call it.
1. Common call mode
let provider = MoyaProvider(endpointClosure: endpointClosure,
requestClosure: requestClosure,
stubClosure: stubClosure,
manager: manager,
plugins: plugins)
provider.request(.createUser("Three"."Zhang")) { result in
do {
let response = try result.get()
let value = try response.mapNSArray()
self.repos = value
} catch {
let printableError = error as CustomStringConvertible
self.showAlert("GitHub Fetch", message: printableError.description)
}
}
Copy the code
2. RxSwift call mode
If you want to use RxSwift, you need to import the library RxMoya, according to the official home page of Moya.
provider.rx.request(.createUser("Three"."Zhang"))
.asObservable()
.mapJSON()
.mapHandyModel(type: UserModel.self)
.asSingle()
.subscribe { (userModel) in
} onFailure: { (error) in
} onDisposed: {
}
.disposable(by:disposable)
Copy the code
3. Secondary packaging of Moya
After reading the above content, we should have a certain understanding of Moya, the actual development, we need to cover quite a lot of things. For example, different interfaces may require different network timeouts, and you may need to configure whether the interface needs to authenticate user information, whether to use local test data, and so on.
Some of them, like baseURL, headers, HTTPMethod, are pretty much the same, but if you set it again every time, and one day you change the address of baseURL, headers will have to add a parameter to it, and then you’ll have to kill people.
1. Extend TargetType protocol
Since Moya already provides TargetType why don’t we extend it?
public protocol BaseHttpAPIManager: TargetType {
/// whether to authenticate the user
var validUser : Bool { get }
/// The timeout period
var timeoutInterval : Double { get }
/// Whether to run the test data default.never
var stubBehavior: Moya.StubBehavior { get }
/ / / and so on...
}
Copy the code
Once protocol inheritance is complete, this is where we can assign values to our largely unchanged parameters.
extension BaseHttpAPIManager {
public var baseURL: URL {
return URL(string: WebService.shared.BaseURL)!
}
public var method: Moya.Method {
return .post
}
public var sampleData: Data {
return "response: test data".data(using: String.Encoding.utf8)!
}
public var task: Task {
return .requestPlain
}
// whether to verify the success code
public var validationType: Moya.ValidationType {
return .successCodes
}
/ / / request header
public var headers: [String : String]? {
return WebService.shared.HttpHeaders
}
/// The following are custom extensions
public var validUser : Bool {
return WebService.shared.ValidUser
}
public var timeoutInterval : Double {
return WebService.shared.TimeoutInterval
}
/// Whether to run the test data default.never
public var stubBehavior: StubBehavior {
return .never
}
/ /...
}
Copy the code
Because the TargetType protocol runs through Moya’s core, you can use it almost anywhere. Then you just need to implement BaseHttpAPIManager protocol compliance.
2. Encapsulate the creation of MoyaProvider
I won’t write the code here, I recommend a GitHub Demo to look at, this dish chicken is also borrowed from here.
4. Use HandyJson
Because HandyJson can support structures. If you do not need to inherit classes in Swift, it is recommended to use structs, which use less memory.
1, the statement
To declare a struct or class, you must support HandyJSON.
struct UserModel : HandyJSON {
var name : String?
var age : Int?
var address : String?
var hobby : [HobbyModel]? /// support model array, but need to write the array type clearly
}
Copy the code
2, use,
/// Common model conversion
let parsedElement = UserModel.deserialize(from: AnyObject)
/// Array model conversion
let parsedArray = [UserModel].deserialize(from: AnyObject)
Copy the code
3. Use in combination with RxSwfit
The extension Observable will do.
public extension Observable where Element : Any {
// convert normal Json to Modelfunc mapHandyModel <T : HandyJSON> (type : T.Type) -> Observable<T? > {return self.map { (element) -> T? in
/// data is String or dic
let data = element
let parsedElement : T?
if let string = data as? String {
parsedElement = T.deserialize(from: string)}else if let dictionary = data as? Dictionary<String , Any> {
parsedElement = T.deserialize(from: dictionary)
} else if let dictionary = data as? [String : Any] {
parsedElement = T.deserialize(from: dictionary)
} else {
parsedElement = nil
}
return parsedElement
}
}
// Convert Json to a model arrayfunc mapHandyModelArray<T: HandyJSON>(type: T.Type) -> Observable<[T?] ? > {return self.map{ (element) -> [T?] ? in/// data is String or diclet data = element let parsedArray : [T?] ?if let string = data as? String {
parsedArray = [T].deserialize(from: string)}else if let array = data as? [Any] {
parsedArray = [T].deserialize(from: array)}else {
parsedArray = nil
}
return parsedArray
}
}
}
Copy the code
Joint mode above 3.3.2 Moya RxSwift call mode is given.
json.rx.mapHandyModel(type: UserModel.self)
.asSingle()
.subscribe { (userModel) in
} onFailure: { (error) in
} onDisposed: {
}
.disposable(by:disposable)
Copy the code
5, RxSwift
See Cooci’s blog on how to use RxSwift.
6, summary
With this, you can quickly create web requests for new projects. If you feel that it helps you in any way, it would be nice to give a thumbs up. Thank you.