前语

咱们开发中,除了开发UI布局相关,别的一个便是和后台沟通的数据相关业务(一般比较耗时的工作会呈现到这儿),而与他们沟通的除了咱们的嘴巴、微信,另一个便是咱们开发用的网络结构 Alamofire

此外开发 Alamfire 运用过程中,以前咱们可能会经常用到 JSONDecode,在 Alamfire 5之后,基本上就用不到了,只需求创建好与后台回来数据相似的模型,运用泛型标记主动转化即可,但实践运用过程中,关于后台回来的巨大的数据,咱们需求定制习惯咱们当前页面的新的数据结构,因而 JSONDecode 还往达不到咱们的规范,因而引出了 HandyJSON,当然还有其他的,这儿只介绍一个

Alamofire源码、HandyJSON源码、SwiftComponentTestDemo测验事例

下面简介一下 json 转模型结构:

ObjectMapper: 面向协议的SwiftJSON 解析结构

HandyJSON: 阿里推出的一个用于Swift的 JSON 序列化/反序列化库。

KakaJSON: mj 新写的一个Swift的JSON转模型东西

Alamfire

Moya 是咱们比较常用的一个基于 Alamfire 二次封装的仓库,但个人感觉没必要在额外运用一个轮子,Alamfire现已做的许多了,直接运用 Alamfire简易封装一个习惯到自己项目的即可,且运用更灵活,当然 Moya看起来设置更简略(咱们对三方的依赖又多了一个),实践上运用不一定合适咱们的风格,尤其是多渠道开发的,下面简略介绍一下 Alamfire 的运用

若对 post 恳求参数类型想多一些了解,能够参考 post恳求上传数据的几种方法

Alamfire 建议 get、post恳求

运用前,先导入

import Alamofire

运用如下所示,method 不传默以为 get,回来回调参数,能够运用泛型设定回来成果的 数据结构

//主动回来decode模型
AF.request("http://onapp.yahibo.top/public/?s=api/test/list", method: .post)
.responseDecodable {(res: AFDataResponse<ResponseList>) in
    switch res.result {
        case .failure(let error):
            print("失利了", error)
        case .success(let model):
            //回来 ResponseList 类型的对象
            print("model", model)
    }
}

泛型 ResponseList 如下所示,解析默许遵从 JSONDecoder 哪一套,因而需求遵从 Codable 协议

//JSONDecoder转模型的设定
//假如运用简略接口,数据比较契合咱们接口,合作泛型,能够直接动态取出运用该模型
//可是碰到复杂的,嵌套,需求自行转化映射出新的数据结构,这个显然不能帮咱们一步到位,需求新的json转模型
//默许不支持泛型,像接口回来的默许的外层 status、message等结构,需求重复编写
struct ResponseList: Codable {
    var status: Int = 0
    var message: String?
    struct item: Codable {
        var id: Int
        var name: String?
        var headimg: String?
        var description: String?
    }
    struct listPage: Codable {
        var page: Int
        var size: Int
        var list: [item]?
    }
    var data: listPage?
}

恳求参数类型

恳求参数默许以字典的方法传递,默许 encoderURLEncodedFormParameterEncoder.default,但也能够传递运用遵从 Encodable 协议的模型,条件是 encoder 改为 JSONParameterEncoder.default,能够依据个人编码风格来走

//默许传参运用字典
let parameters = ["username": "wwwsssddd123", "password": "kweikjkkke12dsda"]
AF.request("http://onapp.yahibo.top/public/?s=api/test/list", 
    method: .post, parameters: parameters)
.response { res in
}
//也支持遵从 Encodable 协议的模型,这样就不必模型转字典了(不过一般也不会独自给参数界说模型吧)
let loginInfo = LoginModel(username: "wwwsss", password: "sdfasdf")
AF.request("http://onapp.yahibo.top/public/?s=api/test/list", 
    method: .post, parameters: loginInfo, encoder: JSONParameterEncoder.default)
.response { res in
}
//LoginModel模型
struct LoginModel: Encodable {
    var username: String
    var password: String
}

传递通用 header

开发过程中,咱们可能在 header 里边添加一些基础信息,例如content-type、timestamp之类的,或者 token 都是有可能的,因而咱们需求做一些特殊处理,统一调度

//这是一个传递 Encodable 参数的事例,而默许字典不需求这个泛型
static func post<Parameters: Encodable>(
        _ convertible: URLConvertible,
         body: Parameters? = nil,
         headers: HTTPHeaders? = nil,
         interceptor: RequestInterceptor? = nil) -> DataRequest {
    //url前缀默许是相同啊了,能够这儿统一拼接,只传递后缀
    let url = "\(HOST)\(convertible)"
    //设置headers
    var headers = HTTPHeaders();
    headers.add(name: "content-type", value: "x-www-form-urlencoded")
    headers.add(name: "type", value: "ios")
    headers.add(name: "timestamp", value: "\(NSDate().timeIntervalSince1970)")
    //回来 AF 默许的 task 供外部持续灵活调度,也能够略微处理一下
    return AF.request(url, method: .post, parameters: body, 
        encoder: JSONParameterEncoder.default, headers: headers, 
        interceptor: interceptor, requestModifier: nil)
}

post恳求(query和body一起上传)

post 恳求过程中,有时候难免会碰到 querybody 一起传参的方法,咱们都知道 get 默许以 query的方法传递, 而 post 默许以 body 的方法传递,有时候后台开发web和移动端公用接口,为了别离一些参数,会两种方法一起运用,因而咱们需求自行预备一下,query参数就需求咱们自行拼接到 url 上面去了

//post上传比较特殊,默许传递body,有时也会query和body一起上传
static func post(_ convertible: URLConvertible,
                 body: Parameters? = nil,
                 parameters: Parameters? = nil,
                 headers: HTTPHeaders? = nil,
                 interceptor: RequestInterceptor? = nil) -> DataRequest {
    var url = "\(HOST)\(convertible)"
    if let parameters = parameters {
        // query   url?key1=value1&key2=value2
        url = "\(url)?"
        var index = 0
        for (key, value) in parameters {
            if index == 0 {
                url = "\(url)?\(key)=\(value)"
            }else {
                url = "\(url)&\(key)=\(value)"
            }
            index += 1
        }
    }
    var headers = HTTPHeaders();
    headers.add(name: "content-type", value: "x-www-form-urlencoded")
    headers.add(name: "type", value: "ios")
    headers.add(name: "timestamp", value: "\(NSDate().timeIntervalSince1970)")
    return AF.request(url , method: .post, parameters: body, 
        encoding: URLEncoding.default, headers: headers, 
        interceptor: interceptor, requestModifier: nil)
}

表单上传 upload

比较大的数据一般都选用表单上传(速度快),这也是咱们一致的计划,网络结构给咱们提供了一个默许的 upload 办法,咱们直接调用即可,其中 multipartFormData 需求咱们自行拼接(不了解实践怎样拼接的,能够参考文章 post恳求上传数据的几种方法)

:假如参数加工成了一般的 base64 字符串,能够直接默许前面 post 传递,参数当字符串即可,没必要非得 upload,这只是一种传递方法

static func upload(multipartFormData: @escaping (MultipartFormData) -> Void,
                       to url: URLConvertible,
                       parameters: [String: Data]?, //二进制的key-value信息
                       usingThreshold encodingMemoryThreshold: UInt64 = MultipartFormData.encodingMemoryThreshold,
                       method: HTTPMethod = .post,
                       headers: HTTPHeaders? = nil,
                       interceptor: RequestInterceptor? = nil,
                       fileManager: FileManager = .default) -> UploadRequest
    {
        let url = "\(HOST)\(url)"
        var headers = HTTPHeaders()
        headers.add(name: "content-type", value: "x-www-form-urlencoded")
        headers.add(name: "type", value: "ios")
        headers.add(name: "timestamp", value: "\(NSDate().timeIntervalSince1970)")
        //上传事例,实践能够直接运用,需求传递 Data类型的参数
        AF.upload(multipartFormData: { multipartFormData in
            if let params = parameters {
                for (key, value) in params {
                    multipartFormData.append(value, withName: key )
                }
            }
        }, to: url, usingThreshold: encodingMemoryThreshold, 
            method: .post, headers: headers, interceptor: interceptor, 
            fileManager: fileManager, requestModifier: nil)
    }

监听网络环境

假如有下载需求时,可能需求监听用户处于 wifi 或者是 蜂窝网络,能够依据状况让用户自行挑选,并且能够在不同状况,需求暂停和康复一些下载使命,监听代码如下所示

//这儿测验一下网络环境
let networkManager = NetworkReachabilityManager(host: "www.yahibo.top")
func testNetworkManager() {
    self.networkManager!.startListening(
        onQueue: DispatchQueue.global(), onUpdatePerforming: { status in
        var message = ""
        switch status {
        case .unknown:
            message = "未知网络"
        case .notReachable:
            message = "无法连接网络"
        case .reachable(.cellular):
            message = "蜂窝移动网络"
        case .reachable(.ethernetOrWiFi):
            message = "WIFI或有线网络"
        }
        print("***********\(message)*********")
    })
}

网络安全验证

前面一些文章有提到 https相关,并且现在都推荐运用 https,因而 https恳求相关的 ssl 证书验证也是必要的,下面简介默许的安全验证的几种手法,咱们假如进行安全恳求,不能运用默许的 AF 了, 假如具体了解 https原理,能够参考这篇文章 https与AFNetworking

//安全验证,设置session,校验规则
//能够将默许的AF调用换成这个能够设置成单例
fileprivate func TrustSession() -> Session{
    //只要一个域名,依据自己的验证方法就填写一个就行
    let policies: [String: ServerTrustEvaluating] = [
        //没有本地项目证书校验,默许服务器信任评估,一起从默许主机列表证书进行校验,不校验证书是否现已失效
        "www.baidu1.com": DefaultTrustEvaluator(),
        //没有本地项目证书校验,查看本地证书的状况,以保证是否失效,这样做会添加开支
        "www.baidu2.com": RevocationTrustEvaluator(),
        //默许,校验本地项目bundle一切证书,能够设置固定证书,第一个参数
        "www.baidu3.com": PinnedCertificatesTrustEvaluator(),
        //默许校验本地项目一切证书的公钥,验证适宜即可
        "www.baidu4.com": PublicKeysTrustEvaluator()
   ]
    let manager = ServerTrustManager(evaluators: policies)
    let session = Session(serverTrustManager: manager)
    // 回来 session,就不必运用默许的 AF了,运用他即可,证书一般也不会容易变更,运用单例即可
    return session
}

网络恳求接口调集(一种偏好事例)

编写网路恳求时,假如咱们想在本地有一个杰出的文档,能够像下面相同,将一个功能模块的接口封装起来,进行注释,这样代码明晰易读,调用方便,因而没必要转化成 model 方法

class Request {
    //cmd + option + / 生成注释模块
    /// 登陆,参数假如是以模型方法保存,也能够以模型方法上传,避免了二次生成字典传问题
    /// - Parameters:
    ///   - username: 用户名
    ///   - password: 暗码
    ///   - successBlock: 成功回调
    ///   - failureBlock: 失利回调
    static func login(username: String,
                      password: String,
                      successBlock: @escaping (_ model: ResponseList)->Void,
                      failureBlock: @escaping ()->Void) {
        let paramters = [
            "username": username,
            "password": password
        ]
        AFNetwork.post("https://www.baidu.com", parameters: paramters).responseDecodable {(res: AFDataResponse<ResponseList>) in
            switch res.result {
                case .failure(let error):
                    print("失利了", error)
                    failureBlock()
                case .success(let model):
                    print("model", model)
                    if (model.status == 200) {
                        successBlock(model)
                    }
            }
        }
    }
    //cmd + option + / 生成注释模块
    /// 登陆,参数假如是以模型方法保存,也能够以模型方法上传,避免了二次生成字典传问题
    /// - Parameters:
    ///   - loginInfo: 用户信息
    ///   - successBlock: 成功回调
    ///   - failureBlock: 失利回调
    static func login(loginInfo: LoginModel,
                      successBlock: @escaping (_ model: ListPageModel?)->Void,
                      failureBlock: @escaping ()->Void) {
        AFNetwork.post("https://www.baidu.com", body: loginInfo).responseString { res in
            switch res.result {
            case .failure(let error):
                print("失利了", error)
                failureBlock()
            case .success(let json):
                //生成成果
                let model = JSONDeserializer<APIResponse<ListPageModel>>.deserializeFrom(json: json)
                if (model?.status == 200) {
                    successBlock(model?.data)
                }
                print("result", model ?? "")
            }
        }
    }
}

HandyJSON

Alamofire 默许的 response 参数,信任咱们现已看到了,运用的默以为 JSONDecode,只能按照原结构进行声明,不能映射(能够添加或者减少字段,可是不能跨层映射参数),且咱们默许的回来类型(status、message之类)重复代码,也无法用泛型规避,因而为了减少一部分重复数据结构,且能自界说属于咱们app当前功能的新的数据结构,那么 HandyJSON 将会是一个不错的挑选(当然 ObjectMapper 、 KakaJSON 也都不错,看自己偏好)

建议恳求并解析

func testHandyJson() {
    let datatask = AF.request("http://onapp.yahibo.top/public/?s=api/test/list", 
        method: .post).responseString { res in
        switch res.result {
        case .failure(let error):
            print("失利了", error)
        case .success(let json):
            //生成成果,这样直接运用,就跟 JSONDecode的相同了,但独自用来json转模型不错,究竟能够映射
            //if let reponse = ResponseList.deserialize(from: jsonString) {
                //print(reponse)
            //}
            //优化成果,运用泛型和映射,将模型生成咱们抱负中的姿态
            let model = JSONDeserializer<APIResponse<ListPageModel>>
                .deserializeFrom(json: json)
            print("result", model ?? "")
        }
    }
    //AF.requestTaskMap 咱们的 task在这儿,假如许多网络使命有需求撤销的话,能够走这儿
    datatask.cancel()
}

上面解析的泛型模型

界说模型,需求解析的模型都需求遵从 HandyJSON 协议

//handyJSON
//运用泛型,默许回来内容如下,咱们直接剔除去外面的信息,需求
//这样咱们就不必重复编写 response 外层的结构代码了
struct APIResponse<T: HandyJSON>: HandyJSON {
    var status: Int = 0
    var message: String?
    var data: T?
//    //这儿是测验映射事例代码,当前事例不应当这么用
//    var page: Int = 0
//    var size: Int = 0
//    //假如不是对应层级,需求进行下面映射
//    //struct需求加上前面的骤变,class不需求
//    mutating func mapping(mapper: HelpingMapper) {
//        mapper <<<
//            self.page <-- "data.page"
//
//        mapper <<<
//            self.size <-- "data.size"
//    }
}
//咱们用到的基本模型,可能需求更多交互,运用class
class PageItem: HandyJSON {
    var id: Int?
    var name: String?
    var headimg: String?
    var description: String?
    required init() {}
}
class ListPageModel: HandyJSON {
    var page: Int = 0
    var size: Int = 0
    var list: [PageItem?]? //由于 PageItem 数据可能异常,因而解析会呈现为空状况,需求界说成可选类型
    required init() {}
}

独自映射模型

除了映射网络恳求回来的数据,有时候还会独自映射数据(例如:咱们保存的默许的json数据,或者是接口某一个参数的json数据),那么咱们还以上面的 ResponseList 结构的json为例,进行映射解析

//假定咱们只需求外层的信息,和里边的 page、size信息,那么能够将 page、size进行下面映射
struct ResponseList: HandyJSON {
    var status: Int = 0
    var message: String?
    //这儿是是作为一个测验事例,这个事例不应当这么用
    var page: Int = 0
    var size: Int = 0
    //假如不是对应层级,需求进行下面映射
    //struct需求加上前面的骤变,class不需求
    mutating func mapping(mapper: HelpingMapper) {
        mapper <<<
            self.page <-- "data.page"
        mapper <<<
            self.size <-- "data.size"
    }
}
//运用代码直接解析,如下所示
if let reponse = ResponseList.deserialize(from: jsonString) {
    print(reponse)
}

最后

依据偏好挑选自己喜爱的三方即可,别的杰出的扩展性、可维护行、运用遍历方面需求平衡,合适大佬们的不一定合适咱们,究竟咱们拥有的时刻成本是不相同的