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) -> URLRequestMethod can be used to concatenate public parameters (version number, token, userID) or dataRSAEncryption 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 therewillSenddidReceiveDo 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.