小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
前言
昨天我介绍了JSON转模型的工具,今天我来介绍一个针对Alamofire4版本添加一个分类,使其通过一个函数进而通过Codable协议转模型的能力。
有兴趣的掘友们可以读一读我之前写的这篇文章:
这篇文章我主要讲解了一些Alamofire5的一些新特性,而这些新特性是Alamofire4不具备的。
为什么到现在了我还在提Alamofire4的代码:
-
因为很多时候,公司的业务代码并没有随着Swift的更新而更新。
-
Alamofire4的代码就算是现在的Xcode上运行也没有任何问题,开发者依旧可以通过熟悉的思路与API进行业务请求。
-
Alamofire4与Alamofire5不管是从API亦或者从设计思路上都有很多的不同,有的时候跨越升级这一步,带来的新版本适配成本是不划算的。
所以一些比较老的项目停留在Alamofire4上面比较正常。
Alamofire+Codable 分类代码
这个是我为Alamofire4编写的分类,扩展其对遵守Codable协议模型的转换方法:
import Foundation
import Alamofire
// MARK: - Codable
extension Request {
/// 返回遵守Codable协议的Result类型
///
/// - Parameters:
/// - response: 服务器返回的响应
/// - data: 服务器返回的数据
/// - error: AFError
/// - keyPath: 模型的keyPath 可解析深层的JSON数据
/// - Returns: Result<T>
public static func serializeResponseCodable<T: Codable>(response: HTTPURLResponse?, data: Data?, error: Error?, keyPath: String?) -> Result<T> {
if let error = error { return .failure(error) }
if let response = response, emptyDataStatusCodes.contains(response.statusCode) {
do {
let value = try JSONDecoder().decode(T.self, from: Data())
return .success(value)
} catch {
return .failure(AFError.responseSerializationFailed(reason: .jsonSerializationFailed(error: error)))
}
}
guard let validData = data else {
return .failure(AFError.responseSerializationFailed(reason: .inputDataNil))
}
if let keyPath = keyPath, !keyPath.isEmpty {
var keyPaths = keyPath.components(separatedBy: "/")
return keyPathForCodable(keyPaths: &keyPaths, data: validData)
}else {
do {
let value = try JSONDecoder().decode(T.self, from: validData)
return .success(value)
} catch {
return .failure(AFError.responseSerializationFailed(reason: .jsonSerializationFailed(error: error)))
}
}
}
/// 通过键值路径寻找深层的JSON对应的模型
///
/// - Parameters:
/// - keyPaths: 路径数组
/// - data: 数据
/// - Returns: Result<T>
private static func keyPathForCodable<T: Codable>(keyPaths: inout [String], data: Data) -> Result<T> {
if let firstKeyPath = keyPaths.first, keyPaths.count > 1 {
keyPaths.remove(at: 0)
if let JSONObject = try? JSONSerialization.jsonObject(with: data, options: .allowFragments),
let keyPathJSONObject = (JSONObject as AnyObject?)?.value(forKeyPath: firstKeyPath),
let keyPathData = try? JSONSerialization.data(withJSONObject: keyPathJSONObject) {
return keyPathForCodable(keyPaths: &keyPaths, data: keyPathData)
}
}else if let lastKeyPath = keyPaths.last, keyPaths.count == 1 {
keyPaths.remove(at: 0)
if let JSONObject = try? JSONSerialization.jsonObject(with: data, options: .allowFragments),
let keyPathJSONObject = (JSONObject as AnyObject?)?.value(forKeyPath: lastKeyPath),
let keyPathData = try? JSONSerialization.data(withJSONObject: keyPathJSONObject) {
do {
let value = try JSONDecoder().decode(T.self, from: keyPathData)
return .success(value)
} catch {
return .failure(AFError.responseSerializationFailed(reason: .jsonSerializationFailed(error: error)))
}
}
}
return .failure(AFError.responseSerializationFailed(reason: .inputDataNil))
}
}
extension DataRequest {
/// 创建一个遵守Codable协议的response serializer
///
/// - Parameter keyPath: 键值路径
/// - Returns: 遵守Codable协议的response serializer
public static func codableResponseSerializer<T: Codable>(keyPath: String?) -> DataResponseSerializer<T> {
return DataResponseSerializer { _, response, data, error in
return Request.serializeResponseCodable(response: response, data: data, error: error, keyPath: keyPath)
}
}
/// 添加一个请求完成的handle
///
/// - Parameters:
/// - queue: 回调线程
/// - keyPath: 键值路径
/// - completionHandler: handle
/// - Returns: DataRequest
@discardableResult
public func responseCodable<T: Codable>(
queue: DispatchQueue? = nil,
keyPath: String? = nil,
completionHandler: @escaping (DataResponse<T>) -> Void)
-> Self
{
return response(
queue: queue,
responseSerializer: DataRequest.codableResponseSerializer(keyPath: keyPath),
completionHandler: completionHandler
)
}
}
private let emptyDataStatusCodes: Set<Int> = [204, 205]
复制代码
就是这么一段不到110行左右的代码,对于网络请求代码的省事省力是肉眼的可见的。
另外这段代码是我写于3年前的,看看Alamofire5中对于Codable的解析,你会觉得大同小异。
我虽然不是首创,不过说实话,Alamofire+ObjectMapper
的代码为这个分类提供90%以上的模版代码。
Alamofire+Codable 使用
例如有下面这样的JSON:
{
"code": 0,
"list": [
{
"topicDesc": "错过线下行前说明会?这里有一份超完整的澳洲行前攻略给你。",
"createTime": null,
"topicImageUrl": "http:\\/\\/cdn.timez1.com\\/20180608\\/18626cd2106344078969afd77acf3572.jpg",
"id": 12,
"topicStatus": 1,
"upTime": "2018-06-13 14:46:06",
"topicOrder": 21,
"topicTittle": "澳洲行前"
},
{
"topicDesc": "错过线下行前说明会?这里有一份超完整的英国行前攻略给你。",
"createTime": null,
"topicImageUrl": "http:\\/\\/cdn.timez1.com\\/20180608\\/d27a7923a24d4cd6878fe08fa338be45.jpg",
"id": 10,
"topicStatus": 1,
"upTime": "2018-06-13 14:46:17",
"topicOrder": 20,
"topicTittle": "英国行前"
}
]
}
复制代码
可以通过JSON转模型工具,我生成如下的Model:
struct Item: Codable {
var topicOrder: Int?
var id: Int?
var topicDesc: String?
var topicTittle: String?
var upTime: String?
var topicImageUrl: String?
var topicStatus: Int?
}
struct Topics: Codable {
var list: [Item]?
var code: Int?
}
复制代码
我们就可以通过Alamofire和我写一个分类方法responseCodable获取其模型了:
/// 获取整个模型
Alamofire.request("请求网址", method: .post).responseCodable { (response: DataResponse<Topics>) in
guard let value = response.value else { return }
print(value)
}
复制代码
当然我们也可以通过其keyPath获取其更底层的模型:
/// keyPath目标的请求Model
Alamofire.request("请求网址", method: .post).responseCodable(queue: nil, keyPath: "list") { (response: DataResponse<[Item]>) in
guard let list = response.value else { return }
print(list)
}
复制代码
是不是感觉有点像Alamofire5那感觉了?
需要注意的是,这种网络请求时,必须显式声明DataResponse的类型,否则就会在这里报错喔。
参考文档
Encoding and Decoding Custom Types
Demo地址
总结
学习他人的代码并合理运用拷贝,并不是一件丢人的事情。
我之所以能成功,是因为我站在巨人的肩上。
伟人如牛顿都如是说,何况我们这些搬砖的码农呢?
我之前为自己写出这个AlamofireCodable窃喜过一整子,不过我立刻通过在GitHub上的搜索知道,有这个想法的人千千万万,实现并开源的代码也千千万万。
我唯一感觉特别快乐的是思考并提炼的这个过程。
我们下期见。