Swift version: 4.1

Xcode Version 9.3 (9E145)

Repackaging based on Alamofire and Moya

Code Github address: MoyaDemo

One, foreword

Recently, I entered a new company to carry out a new project. I found that the network layer of the company’s project is very difficult. The most unbearable thing is that data parsing is outside the network layer, and each data model needs to write parsing code separately. While the project was just getting started, I proposed that I write a network-layer gadget to replace the old network-layer, with loading chrysanthemums and caching wrapped in it.

Moya tools and Codable Protocols

This is a guide to Moya and cidr protocols for which you may be interested. Check them out on your own for more information.

2.1 Moya tools

I used Moya because I found it convenient, and if the reader doesn’t want to use Moya, it won’t stop you from reading this article.

Alamofire won’t be covered here, but if you haven’t already, you can think of it as AFNetworking for Swift. Moya is a repackaged library of Alamofire tools. If you use Alamofire alone, your web request might look like this:

let url = URL(string: "your url")!
Alamofire.request(url).response { (response) in
    // handle response
}
Copy the code

Of course, the reader will also be based on the secondary encapsulation, not just the above code as simple.

With Moya, the first thing you do is not ask directly, but create a file definition interface based on the project module. For example, I like to name the module + API according to its function, and then define the interface we need to use in it, for example:

import Foundation
import Moya

enum YourModuleAPI {
    case yourAPI1
    case yourAPI2(parameter: String)}extension YourModuleAPI: TargetType {
    var baseURL : URL {
        return URL(string: "your base url")!
    }
    
    var headers : [String : String]? {
        return "your header"
    }
    
    var path: String {
        switch self {
            case .yourAPI1:
                return "yourAPI1 path"
            case .yourAPI2:
                return "yourAPI2 path"}}var method: Moya.Method {
        switch self {
            case .yourAPI1:
                return .post
            default:
                return .get}}// This is just a network request with parameters
    var task: Task {
        var parameters: [String: Any] = [:]
        switch self {
            case let .yourAPI1:
                parameters = [:]
            case let .yourAPI2(parameter):
                parameters = ["Field":parameter]
        }
        return .requestParameters(parameters: parameters,
                                    encoding: URLEncoding.default)}// unit test use
    var sampleData : Data {
        return Data()}}Copy the code

Once the above files are defined, you can make network requests as follows:

MoyaProvider<YourModuleAPI>().request(YourModuleAPI.yourAPI1) { (result) in
    // handle result            
}
Copy the code

2.2 Codable agreement

The Codable protocol is a Swift4 update for interpreting and encoding data. It is made up of an encoding protocol and a decoding protocol.

public typealias Codable = Decodable & Encodable
Copy the code

Before Swift updated the Codable protocol, I used SwiftyJSON to parse data returned from network requests. After using Codable, I started using it directly.

There are some holes in Codable protocols, though, as this article describes:

When JSONDecoder meets the real world, things Get ugly…

The following Person model class stores a simple personal information, which is only decoded, so only the Decodable protocol is obeyed:

struct Person: Decodable {
  var name: String
  var age: Int
}
Copy the code

String and Int are the system’s default codec types, so we don’t need to write any more code, and the compiler will implement them for us by default.

let jsonString = """{"name":"swordjoy","age": 99}"""

if let data = jsonString.data(using: .utf8) {
    let decoder = JSONDecoder()
    if let person = try? decoder.decode(Person.self, from: data) {
        print(person.age)    // 99
        print(person.name)   // swordjoy
    }
}
Copy the code

All you need to do is pass the Person type to the JSONDecoder object, which directly converts JSON data into a Person data model object. In practice, due to the strict restrictions of parsing rules, it is not as easy as it looks above.

Analysis and solutions

3.1.1 Repeatedly parses data into the model

For example, there are two interfaces, one is to request the list of goods, one is to request the mall home page. I used to write like this:

enum MallAPI {
    case getMallHome
    case getGoodsList
}
extension MallAPI: TargetType {
    / / a little
}
Copy the code
let mallProvider = MoyaProvider<MallAPI>()
mallProvider.request(MallAPI.getGoodsList) { (response) in
    // Parse response into Goods model array with success closure
}

mallProvider.request(MallAPI.getMallHome) { (response) in
    // Parse the response into a Home model and pass out the success closure
}
Copy the code

The above is a simplified practical scenario where each network request is written separately to parse the returned data into a data model or an array of data models. Even encapsulating the functionality of data parsing into a singleton utility class is only marginally better.

What the author wants is that after specifying the data model type, the network layer directly returns the parsed data model for our use.

3.1.2 Use generics to solve

Generics solve this problem by using generics to create a web utility class that is subject to the Codable protocol.

struct NetworkManager<T> where T: Codable {}Copy the code

This allows us to specify the data model type to be parsed when we use it.

NetworkManager<Home>().reqest...
NetworkManager<Goods>().reqest...
Copy the code

Careful readers will notice that this is used in the same way that Moya initializes the MoyaProvider class.

3.2.1 How to encapsulate the loading controller and cache at the network layer after using Moya

Since Moya is used for reencapsulation, the cost of each encapsulation of code is a sacrifice of degrees of freedom. How does loading controller & caching fit with Moya?

A simple way to do this is to add whether to display the controller and whether to cache Boolean values to the request method. Seeing that my request method has 5 or 6 parameters, this option is immediately ruled out. Looking at Moya’s TargetType protocol gave me inspiration.

3.2.2 Use the protocol to solve the problem

Since MallAPI can comply with TargetType to configure network request information, it can also comply with our own protocol to do some configuration.

Customize a supplementary protocol for Moya

protocol MoyaAddable {
    var cacheKey: String? { get }
    var isShowHud: Bool { get}}Copy the code

MallAPI then has to comply with two protocols

extension MallAPI: TargetType.MoyaAddable {
    / / a little
}
Copy the code

Part of the code display and analysis

The complete code can be downloaded from Github.

4.1 Encapsulated Network Request

Given the data type to be returned, the returned Response can directly call the dataList property to obtain the parsed Goods data model array. Error closures can also be used to retrieve error messages directly from error. Message, and then choose whether to use pop-up boxes to prompt users based on business requirements.

NetworkManager<Goods>().requestListModel(MallAPI.getOrderList, 
completion: { (response) in
    letlist = response? .dataListletpage = response? .page }) { (error)in
    if let msg = error.message else {
        print(msg)
    }
}
Copy the code

4.2 Encapsulation of returned data

The data structure returned by the author’s server is roughly as follows:

{
    "code": 0."msg": "Success"."data": {
        "hasMore": false."list": []}}Copy the code

For current business and data parsing purposes, I have encapsulated the returned data types into two categories, and also included the parsed operations.

The latter request method is also divided into two, which are not necessary and can be chosen according to the reader’s business and preference.

  • Data returned by the request list interface
  • Request data returned by the normal interface
class BaseResponse {
    var code: Int{... }/ / parsing
    var message: String? {... }/ / parsing
    var jsonData: Any? {... }/ / parsing
    
    let json: [String : Any]
    init? (data:Any) {
        guard let temp = data as? [String : Any] else {
            return nil
        }
        self.json = temp
    }
    
    func json2Data(_ object: Any) -> Data? {
        return try? JSONSerialization.data(
        withJSONObject: object,
        options: [])
    }
}

class ListResponse<T> :BaseResponse where T: Codable {
    var dataList: [T]? {... }/ / parsing
    var page: PageModel? {... }/ / parsing
}

class ModelResponse<T> :BaseResponse where T: Codable {
    var data: T? {... }/ / parsing
}
Copy the code

So we can just return the corresponding wrapper object and get the parsed data.

4.3 Incorrect encapsulation

In the process of network request, there must be all kinds of errors, here using the Swift language error mechanism.

// Network error handling enumeration
public enum NetworkError: Error  {
    / / a little...
    // The server returned an error
    case serverResponse(message: String? , code:Int)}extension NetworkError {
    var message: String? {
        switch self {
            case let .serverResponse(msg, _) :return msg
            default: return nil}}var code: Int {
        switch self {
            case let .serverResponse(_, code): return code
            default: return -1}}}Copy the code

The extension here is important to help us get the wrong message and code when handling errors.

4.4 Request network method

The method of the final request

private func request<R: TargetType & MoyaAddable>(
    _ type: R,
    test: Bool = false,
    progressBlock: ((Double)- > ())? =nil,
    modelCompletion: ((ModelResponse<T>?) -> ())? = nil,
    modelListCompletion: ((ListResponse<T>? - > ())? =nil,
    error: @escaping (NetworkError) -> ()) ->Cancellable?
{}
Copy the code

The R generic here is used to retrieve the interface defined by Moya, specifying that both TargetType and MoyaAddable protocols must be followed, and the rest is routine. As with encapsulated return data, there is a plain interface and a list interface.

@discardableResult
func requestModel<R: TargetType & MoyaAddable>(
    _ type: R,
    test: Bool = false,
    progressBlock: ((Double)- > ())? =nil,
    completion: @escaping ((ModelResponse<T>? -> ()), error: @escaping (NetworkError) -> ()) ->Cancellable?
{
    return request(type,
                    test: test,
                    progressBlock: progressBlock,
                    modelCompletion: completion,
                    error: error)
}

@discardableResult
func requestListModel<R: TargetType & MoyaAddable>(
    _ type: R,
    test: Bool = false,
    completion: @escaping ((ListResponse<T>?) -> ()),
    error: @escaping (NetworkError) -> ()) ->Cancellable?
{
    return request(type,
                    test: test,
                    modelListCompletion: completion,
                    error: error)
}
Copy the code

I made this list a bit too rigid for one that is both a list and some other data based on what I already know about projects and Codable agreements. In time, however, you can add a method like this to send the data out for processing.

// There is no such method in Demo
func requestCustom<R: TargetType & MoyaAddable>(
    _ type: R,
    test: Bool = false,
    completion: (Response)- > () - >Cancellable? 
{
    / / a little
}
Copy the code

4.5 Caching and Loading controllers

After adding the MoyaAddable protocol, there is nothing more difficult to do. You can simply get the configuration from the interface definition file according to type.

var cacheKey: String? {
    switch self {
        case .getGoodsList:
            return "cache goods key"
        default:
            return nil}}var isShowHud: Bool {
    switch self {
        case .getGoodsList:
            return true
        default:
            return false}}Copy the code

This adds two functions in the getGoodsList interface request

  • The request returns data through the given cacheKeycaching
  • Automatically show and hide load controller during network request.

If the reader’s load controller has a different style, you can also add a load controller style property. Even whether the caching is synchronous or asynchronous can be added through this MoyaAddable.

/ / cache
private func cacheData<R: TargetType & MoyaAddable>(
    _ type: R,
    modelCompletion: ((Response<T>?)- > ())? =nil,
    modelListCompletion: ( (ListResponse<T>? - > ())? =nil,
    model: (Response<T>? .ListResponse<T>?))
{
    guard let cacheKey = type.cacheKey else {
        return
    }
    ifmodelComletion ! =nil.let temp = model.0 {
        / / cache
    }
    ifmodelListComletion ! =nil.let temp = model.1 {
        / / cache}}Copy the code

The loading controller is shown and hidden using Moya’s own plug-in tool.

// Create moya request class
private func createProvider<T: TargetType & MoyaAddable>( type: T, test: Bool) 
    -> MoyaProvider<T> 
{
    let activityPlugin = NetworkActivityPlugin { (state, targetType) in
        switch state {
        case .began:
            DispatchQueue.main.async {
                if type.isShowHud {
                    SVProgressHUD.showLoading()
                }
                self.startStatusNetworkActivity()
            }
        case .ended:
            DispatchQueue.main.async {
                if type.isShowHud {
                    SVProgressHUD.dismiss()
                }
                self.stopStatusNetworkActivity()
            }
        }
    }
    let provider = MoyaProvider<T>(
        plugins: [activityPlugin,
        NetworkLoggerPlugin(verbose: false)])
    return provider
}
Copy the code

4.6 Avoiding Repeated Requests

An array is defined to hold information about network requests, and a parallel queue uses the Barrier function to keep array elements added and removed thread safe.

// It is used to process the fenced queue with only one request
private let barrierQueue = DispatchQueue(label: "cn.tsingho.qingyun.NetworkManager",
attributes: .concurrent)
// An array is used to process only one request, holding unique information about the request
private var fetchRequestKeys = [String] ()Copy the code
private func isSameRequest<R: TargetType & MoyaAddable>(_ type: R) -> Bool {
    switch type.task {
        case let .requestParameters(parameters, _) :let key = type.path + parameters.description
            var result: Bool!
            barrierQueue.sync(flags: .barrier) {
                result = fetchRequestKeys.contains(key)
                if! result { fetchRequestKeys.append(key) } }return result
        default:
            // will not be called
            return false}}private func cleanRequest<R: TargetType & MoyaAddable>(_ type: R) {
    switch type.task {
        case let .requestParameters(parameters, _) :let key = type.path + parameters.description
            barrierQueue.sync(flags: .barrier) {
                fetchRequestKeys.remove(key)
            }
        default:
            // will not be called()}}Copy the code

There is a small problem with this implementation, multiple interfaces using the same interface, and the same parameters, will only be requested once, but this is still rare, not encountered for the moment there is no processing.

Five, the afterword.

The network layer code currently packaged is a bit of a business type, after all my original intention is to rewrite the network layer for my own company project, so it may not be suitable for some situations. However, the use of generics and protocols is common here, and readers can implement a network layer that matches their project in the same way. If readers have better suggestions, we hope to comment on them together.

Reprint comments leave reprint address can be reprinted.