Swift uses protocol generics to encapsulate the network layer

swift version: 4.1

Xcode version 9.3 (9E145)

Repackage based on Alamofire and Moya

Code Github address: MoyaDemo

I. Introduction

I recently entered a new company to carry out a new project. I found that the network layer of the company's project is very OC. The most unbearable thing is that the data parsing is outside the network layer, and each data model needs to write parsing code separately. While the project is just starting, I propose that I write a network layer gadget to replace the previous network layer, and by the way, the loading chrysanthemum and the cache are also encapsulated in it.

2. Introduction to Moya tools and Codable protocol

Here is just to show Moyathe basic usage of and Codable协议the basic knowledge of . If you are interested in these two, readers can search and study by themselves.

2.1 Moya Tools

MoyaUse because the author thinks it is very convenient, if the reader does not want to use it, it will Moyanot affect your reading of the content of this article.

AlamofireI won't introduce it here. If you haven't touched it, you can regard it as a Swiftversion AFNetworking. MoyaIt is Alamofirea tool library that repackages . If only used Alamofire, your network request might look like this:

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

Of course, readers will also perform secondary encapsulation based on it, not just the above code.

If you use Moyait, the first thing you do is not to directly request, but to establish a file definition interface according to the project module. For example, I like to name the module according to the function 模块名 + API, 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
        }
    }
    
    // 这里只是带参数的网络请求
    var task: Task {
        var parameters: [String: Any] = [:]
        switch self {
            case let .yourAPI1:
                parameters = [:]
            case let .yourAPI2(parameter):
                parameters = ["字段":parameter]
        }
        return .requestParameters(parameters: parameters,
                                    encoding: URLEncoding.default)
    }
    
    // 单元测试使用    
    var sampleData : Data {
        return Data()
    }
}

After defining the above file, you can use the following methods to make network requests:

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

2.2 Codable protocol

Codable协议It is Swift4only updated and used to parse and encode data. It consists of an encoding protocol and a decoding protocol.

public typealias Codable = Decodable & Encodable

Before the Swiftupdate Codable协议, the author has been using SwiftyJSON to parse the data returned by the network request. Codable协议After using it recently , I found it to be quite easy to use, so I used it directly.

However, there are Codable协议still some pitfalls, such as described in this article:

When JSONDecoder meets the real world, things get ugly…

The following Personmodel class stores a simple personal information, here only uses the decoding, so only complies with Decodable协议:

struct Person: Decodable {
  var name: String
  var age: Int
}

StringAnd Intis the default codec type of the system, so we don't need to write other code, the compiler will implement it 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
    }
}

Just pass the Persontype to the JSONDecoderobject, and it can directly convert the JSONdata into a Persondata model object. In actual use, due to various strict limitations of parsing rules, it is far less simple than it looks above.

3. Analysis and Solutions

3.1.1 Repeated parsing data to model

For example, there are two interfaces here, one is to request the product list, and the other is to request the homepage of the mall. I wrote this before:

enum MallAPI {
    case getMallHome
    case getGoodsList
}
extension MallAPI: TargetType {
    // 略   
}
let mallProvider = MoyaProvider<MallAPI>()
mallProvider.request(MallAPI.getGoodsList) { (response) in
    // 将 response 解析成 Goods 模型数组用 success 闭包传出去
}

mallProvider.request(MallAPI.getMallHome) { (response) in
    // 将 response 解析成 Home 模型用 success 闭包传出去
}

The above is a simplified practical scenario. Each network request will be written once to parse the returned data into a data model or data model array. Even encapsulating the function of data parsing into a singleton tool class is only slightly better.

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

3.1.2 Use generics to solve

Generics are used to solve the above problem, use generics to create a network tool class, and give generic constraints: follow the Codableprotocol .

struct NetworkManager<T> where T: Codable {
    
}

In this way, when we use it, we can specify the type of data model that needs to be parsed.

NetworkManager<Home>().reqest...
NetworkManager<Goods>().reqest...

The attentive reader will notice that this is the same way as the MoyaInitializer MoyaProviderclass is used.

3.2.1 After using Moya, how to encapsulate the load controller and cache to the network layer

Due to the use Moyaof re-encapsulation, the cost of each encapsulation of the code is the sacrifice of degrees of freedom. How to Moyafit ?

A very simple way is to add whether to display the controller and whether to cache boolean parameters in the request method. Looking at my request method parameters already 5 or 6, this solution was immediately ruled out. Looking at Moyathe TargetTypeprotocol gave me inspiration.

3.2.2 Use the protocol to solve

Since it MallAPIcan comply with TargetTypethe configuration network request information, of course, we can also comply with our own protocol to perform some configuration.

Customize Moyaa Supplemental Protocol

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

In this way MallAPI, two protocols need to be followed.

extension MallAPI: TargetType, MoyaAddable {
    // 略   
}

Fourth, part of the code display and analysis

The complete code, readers can Githubgo to download.

4.1 Encapsulated network request

By giving the data type to be returned, the returned responsecan directly call the dataListproperty to obtain the parsed Goodsdata model array. In the error closure, the error information can also be error.messageobtained , and then choose whether to use a pop-up box to prompt the user according to business requirements.

NetworkManager<Goods>().requestListModel(MallAPI.getOrderList, 
completion: { (response) in
    let list = response?.dataList
    let page = response?.page
}) { (error) in
    if let msg = error.message else {
        print(msg)
    }
}

4.2 Encapsulation of returned data

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

{
    "code": 0,
    "msg": "成功",
    "data": {
        "hasMore": false,
        "list": []
    }
}

For the consideration of the current business and parsing data, the author encapsulates the returned data types into two categories, and also puts the parsing operation in it.

The following request methods are also divided into two, which is not necessary, readers can choose according to their own business and preferences.

  • Request the data returned by the list interface
  • Request the data returned by the common interface
class BaseResponse {
    var code: Int { ... } // 解析
    var message: String? { ... } // 解析
    var jsonData: Any? { ... } // 解析
    
    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]? { ... } // 解析
    var page: PageModel? { ... } // 解析
}

class ModelResponse<T>: BaseResponse where T: Codable {
    var data: T? { ... } // 解析
}

In this way, we can directly return the corresponding encapsulated class object to obtain the parsed data.

4.3 Wrong encapsulation

In the process of network request, there must be various errors, and the error mechanism of the Swiftlanguage .

// 网络错误处理枚举
public enum NetworkError: 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
        }
    }
}

The extension here is important, it helps us get the wrong messageand code.

4.4 Request network method

method of 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?
{}

The Rgeneric type is used to obtain Moyathe defined interface, specifying that the TargetTypeand MoyaAddableprotocol must be followed at the same time, and the rest are normal operations. Like the encapsulated return data, there are also common interfaces and list interfaces.

@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)
}

I considered the pit points of the current project and Codableprotocol , and wrote it a bit rigidly, in case there is a list and other data, it is not applicable. However, a method similar to this can be added at that time to transmit the data for processing.

// Demo里没有这个方法
func requestCustom<R: TargetType & MoyaAddable>(
    _ type: R,
    test: Bool = false,
    completion: (Response) -> ()) -> Cancellable? 
{
    // 略
}

4.5 Cache and Load Controllers

After thinking of adding a MoyaAddableprotocol , there is no difficulty in other things, just typemake corresponding operations directly according to the configuration in the interface definition file.

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
    }
}

This adds two functions in the getGoodsListinterface request

  • After the request returns data, it will Keybe cache
  • Automatically show and hide loading controllers during network requests.

If the reader's loading controller has a different style, you can also add a property for the loading controller's style. Even whether the cache is synchronous or asynchronous can be MoyaAddableadded .

// 缓存
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
    }
    if modelComletion != nil, let temp = model.0 {
        // 缓存
    }
    if modelListComletion != nil, let temp = model.1 {
        // 缓存
    }
}

The loading controller is shown and hidden using the Moyabuilt -in plugin tool.

// 创建moya请求类
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
}

4.6 Avoid Duplicate Requests

Define an array to hold information about network requests, and a parallel queue using barrierfunctions to ensure thread-safe addition and removal of array elements.

// 用来处理只请求一次的栅栏队列
private let barrierQueue = DispatchQueue(label: "cn.tsingho.qingyun.NetworkManager",
attributes: .concurrent)
// 用来处理只请求一次的数组,保存请求的信息 唯一
private var fetchRequestKeys = [String]()
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:
            // 不会调用
            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:
            // 不会调用
            ()
    }
}

There is a small problem with this implementation method. If multiple interfaces use the same interface and the parameters are the same, they will only be requested once. However, this situation is still very rare, and it will not be dealt with if it is not encountered for the time being.

5. Postscript

The currently encapsulated network layer code is a bit of a strong business type. After all, my original intention was to rewrite a network layer for my company's project, so it may not be suitable for some situations. However, the method of using generics and protocols here is general, and readers can use the same method to implement the network layer that matches their own projects. If readers have better suggestions, they also hope to comment and discuss together.

Reprint comments and leave the reprint address to reprint.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326093391&siteId=291194637