使用协议的 Swift 通用解码器

Posted

技术标签:

【中文标题】使用协议的 Swift 通用解码器【英文标题】:Generic Decoder for Swift using a protocol 【发布时间】:2020-01-13 15:55:33 【问题描述】:

我尝试使用协议为我的所有模型使用通用 Json 解码器。

//这里是协议的定义:

func fetch<T: Decodable>(with request: URLRequest, decode: @escaping (Decodable) -> T?, completion: @escaping (Result<T, APIError>) -> Void) .. other Code

//这里实现:

func getData(from endPoint: Endpoint, completion: @escaping (Result<ApiResponseArray<Codable>, APIError>) -> Void) 

        let request = endPoint.request

        fetch(with: request, decode:  json -> Decodable in
           guard let dataResult = json as? modelData else  return  nil 
           return dataResult
        , completion: completion)
    

ApiResponseArray 给我错误:协议类型“可编码”(又名“可解码和可编码”)不能符合“可解码”,因为只有具体类型才能符合协议。但是我怎样才能实现一个通用的解码器并传递给他们不同的模型。我想我必须修改我的协议定义,但是如何修改?我想传递模型,然后接收模型的解码数据(在我的示例 modelData 中)。很明显,当我编写时程序运行: func getData(from endPoint: Endpoint, completion: @escaping(Result, APIError>) 我的意思是当我使用具体的Model时,但我想传递模型,以便我可以将类用于不同的模型。

谢谢, 阿诺德

【问题讨论】:

阅读本系列文章:robnapier.net/start-with-a-protocol 感谢您的提示。但也许你也可以给我一个建议,如何编写完成处理程序来处理通用模型。 【参考方案1】:

协议不能符合自己,Codable 必须是具体类型或只能用作泛型约束。

在你的上下文中你必须做后者,像这样

func fetch<T: Decodable>(with request: URLRequest, decode: @escaping (Data) throws -> T, completion: @escaping (Result<T, APIError>) -> Void)   

func getData<T: Decodable>(_ : T.Type = T.self, from endPoint: Endpoint, completion: @escaping (Result<T, APIError>) -> Void) 

    let request = endPoint.request

    fetch(with: request, decode:  data -> T  in
        return try JSONDecoder().decode(T.self, from: data)
    , completion: completion)

网络请求通常返回Data,作为decode闭包的参数类型更合理

【讨论】:

我刚刚学到的一个让这变得更好的技巧:将getData() 的第一个参数设为_: T.Type = T.self。这样,您可以在函数签名中传递类型,而不必将其包含在闭包中(这通常有点难看)。但如果很容易推断,你就不必通过了。 @RobNapier 听起来确实不错。以及如何使用函数来推断类型? 如果你传递一个文字闭包,那么你可以称之为getData(Something.self, from: endpoint) ... ),你不需要把类型放在闭包中。如果你传递一个闭包变量(所以类型是已知的),你可以调用getData(from: endpoint, completion: completion)【参考方案2】:

我可以向您建议如何通过使用AlamofireDecodable 与您的API 调用结构一起使用。

我创建了继承自SessionManagerRequestManager 类,并在其中添加了对所有人通用的请求调用。

class RequestManager: SessionManager 

    // Create shared instance
    static let shared = RequestManager()

    // Create http headers
    lazy var httpHeaders : HTTPHeaders = 
        var httpHeader = HTTPHeaders()
        httpHeader["Content-Type"] = "application/json"
        httpHeader["Accept"] = "application/json"
        return httpHeader
    ()


    //------------------------------------------------------------------------------
    // MARK:-
    // MARK:- Request Methods
    //------------------------------------------------------------------------------

    func responseRequest(_ url: String, method: Alamofire.HTTPMethod, parameter: Parameters? = nil, encoding: ParameterEncoding, header: HTTPHeaders? = nil, completionHandler: @escaping (DefaultDataResponse) -> Void) -> Void 

        self.request(url, method: method, parameters: parameter, encoding: encoding, headers: header).response  response in
            completionHandler(response)
        
    
 

然后在另一个类创建 NetworkManager 类之后,该类包含所需的 get/post 方法调用并由 JSONDecoder 解码 json,如下所示:

class NetworkManager 

    static let shared                   = NetworkManager()
    var progressVC                      : ProgressVC?

    //----------------------------------------------------------------
    // MARK:-
    // MARK:- Get Request Method
    //----------------------------------------------------------------

    func getResponse<T: Decodable>(_ url: String, parameter: Parameters? = nil, encoding: ParameterEncoding = URLEncoding.default, header: HTTPHeaders? = nil, showHUD: HUDFlag = .show, message: String? = "Please wait...", decodingType: T.Type, completion: @escaping (Decodable?, APIError?) -> Void) 

        DispatchQueue.main.async 
            self.showHideHud(showHUD: showHUD, message: "")
        

        RequestManager.shared.responseRequest(url, method: .get, parameter: parameter, encoding: encoding, header: header)  response in

            DispatchQueue.main.async 
                self.showHideHud(showHUD: .hide, message: "")
            

            guard let httpResponse = response.response else 
                completion(nil, .requestFailed("Request Failed"))
                return
            

            if httpResponse.statusCode == 200 
                if let data = response.data 
                    do 
                        let genericModel = try JSONDecoder().decode(decodingType, from: data)
                        completion(genericModel, nil)
                     catch 

                        do 

                            let error = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any]

                            if let message = error!["message"] as? String 

                                completion(nil, .errorMessage(message)!)

                             else if let message = error!["message"] as? Int 

                                completion(nil, .errorMessage(String(describing: "Bad Request = \(message)")))
                            

                         catch 
                            completion(nil, .jsonConversionFailure("JSON Conversion Failure"))
                        
                    
                 else 
                    completion(nil, .invalidData("Invalid Data"))
                
             else 
                completion(nil, .responseUnsuccessful("Response Unsuccessful"))
            
        
    

ProgressVC 是我的自定义类,用于在 api 调用时显示进度视图。

之后,我创建了DataManager 类,它将帮助我创建请求 url。

class DataManager: NSObject 

    //------------------------------------------------------------------------------
    // MARK:- Variables
    //------------------------------------------------------------------------------

    static let shared   = DataManager()
    let baseUrl         = WebServiceURL.local    


    //------------------------------------------------------------------------------
    // MARK:- Custom Methods
    //------------------------------------------------------------------------------

    // Get API url with endpoints
    func getURL(_ endpoint: WSEndPoints) -> String 
        return baseUrl + endpoint.rawValue
    

我创建了以下枚举以在我的完成块中发送数据或错误。

enum Result<T, U> where U: Error  
    case success(T)
    case failure(U)

这里是存储与 api 调用期间触发的状态相关的自定义消息的错误列表。

enum APIError: Error 
    case errorMessage(String)
    case requestFailed(String)
    case jsonConversionFailure(String)
    case invalidData(String)
    case responseUnsuccessful(String)
    case jsonParsingFailure(String)

    var localizedDescription: String 

        switch self 

        case.errorMessage(let msg):
            return msg

        case .requestFailed(let msg):
            return msg

        case .jsonConversionFailure(let msg):
            return msg

        case .invalidData(let msg):
            return msg

        case .responseUnsuccessful(let msg):
            return msg

        case .jsonParsingFailure(let msg):
            return msg
        
    

然后,我将扩展这个DataManager 类来调用基于模块的Web 服务。所以我将创建 Swift 文件并扩展 DataManager 类并调用相关 API。

见下文,在 API 调用中,我会将相关模型返回到 Result,如 Result&lt;StoreListModel, APIError&gt;

extension DataManager 

    // MARK:- Store List
    func getStoreList(completion: @escaping (Result<StoreListModel, APIError>) -> Void) 

        NetworkManager.shared.getResponse(getURL(.storeList), parameter: nil, encoding: JSONEncoding.default, header: getHeaders("bd_suvlascentralpos"), showHUD: .show, message: "Please wait...", decodingType: StoreListModel.self)  (decodableData, apiError) in

            if apiError != nil 

                completion(.failure(apiError!))

             else 

                guard let userData = decodableData as? StoreListModel else 
                    completion(.failure(apiError!))
                    return
                

                completion(.success(userData))
            
        
    

从请求的完成块中,我将获得可解码的数据,在这里可以安全地键入 cast。

用途:

DataManager.shared.getStoreList  (result) in

        switch result 

        case .success(let storeListModel):

            if let storeList = storeListModel, storeList.count > 0 
                self.arrStoreList = storeList

                self.tblStoreList.isHidden = false
                self.labelEmptyData.isHidden = true

                self.tblStoreList.reloadData()
             else 
                self.tblStoreList.isHidden = true
                self.labelEmptyData.isHidden = false
            
            break

        case .failure(let error):
            print(error.localizedDescription)
            break
        
    

注意:-一些变量,模型类是我自定义的。你可以用你的替换它。

【讨论】:

以上是关于使用协议的 Swift 通用解码器的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Swift 4 中引用通用的可解码结构

Swift 4 JSONDecoder 解码协议类型

使用 JSONEncoder 编码/解码符合协议的类型数组

使用 Swift 4 的 Decodable 解码 Void

如何从 Swift 4 中的解码器容器中获取未解码的属性?

如果单元素解码失败,Swift JSONDecode 解码数组将失败