前言
我们开发中,除了开发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?
}
请求参数类型
请求参数默认以字典的形式传递,默认 encoder
为 URLEncodedFormParameterEncoder.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
请求过程中,有时候难免会碰到 query
和 body
同时传参的形式,我们都知道 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)
}
最后
根据偏好选择自己喜欢的三方即可,另外良好的扩展性、可维护行、使用遍历方面需要平衡,适合大佬们的不一定适合我们,毕竟大家拥有的时间成本是不一样的