swift中Alamofire以及HandyJSON的使用

2,021 阅读11分钟

前言

我们开发中,除了开发UI布局相关,另外一个就是和后台沟通的数据相关业务(一般比较耗时的工作会出现到这里),而与他们沟通的除了我们的嘴巴、微信,另一个就是我们开发用的网络框架 Alamofire

此外开发 Alamfire 使用过程中,以前我们可能会经常用到 JSONDecode,在 Alamfire 5之后,基本上就用不到了,只需要创建好与后台返回数据相似的模型,使用泛型标记自动转化即可,但实际使用过程中,对于后台返回的庞大的数据,我们需要定制适应我们当前页面的新的数据结构,因此 JSONDecode 还往达不到我们的标准,因此引出了 HandyJSON,当然还有其他的,这里只介绍一个

Alamofire源码HandyJSON源码SwiftComponentTestDemo测试案例

下面简介一下 json 转模型框架:

ObjectMapper: 面向协议的 Swift JSON 解析框架

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 将会是一个不错的选择(当然 ObjectMapperKakaJSON 也都不错,看自己偏好)

发起请求并解析

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

最后

根据偏好选择自己喜欢的三方即可,另外良好的扩展性、可维护行、使用遍历方面需要平衡,适合大佬们的不一定适合我们,毕竟大家拥有的时间成本是不一样的