为Alamofire4添加Codable协议扩展

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

前言

昨天我介绍了JSON转模型的工具,今天我来介绍一个针对Alamofire4版本添加一个分类,使其通过一个函数进而通过Codable协议转模型的能力。

有兴趣的掘友们可以读一读我之前写的这篇文章:

Swift:网络请求库——Alamofire

这篇文章我主要讲解了一些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 

Using JSON with Custom Types 

Swift:网络请求库——Alamofire

AlamofireObjectMapper

Demo地址

AlamofireCodable

总结

学习他人的代码并合理运用拷贝,并不是一件丢人的事情。

我之所以能成功,是因为我站在巨人的肩上。

伟人如牛顿都如是说,何况我们这些搬砖的码农呢?

我之前为自己写出这个AlamofireCodable窃喜过一整子,不过我立刻通过在GitHub上的搜索知道,有这个想法的人千千万万,实现并开源的代码也千千万万。

我唯一感觉特别快乐的是思考并提炼的这个过程。

我们下期见。

猜你喜欢

转载自juejin.im/post/7015586279798603790