从 REST API 客户端返回一个通用的 swift 对象

Posted

技术标签:

【中文标题】从 REST API 客户端返回一个通用的 swift 对象【英文标题】:Return a generic swift object from a REST API Client 【发布时间】:2018-02-07 06:10:34 【问题描述】:

我正在实现一个 API 客户端,它将调用我的后端 API,并返回适当的对象或错误。

这是我目前所拥有的:

public typealias JSON = [String: Any]
public typealias HTTPHeaders = [String: String]

public enum RequestMethod: String 
    case get = "GET"
    case post = "POST"
    case put = "PUT"
    case delete = "DELETE"


public class APIClient 
    public func sendRequest(_ url: String,
                             method: RequestMethod,
                             headers: HTTPHeaders? = nil,
                             body: JSON? = nil,
                             completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) 
        let url = URL(string: url)!
        var urlRequest = URLRequest(url: url)
        urlRequest.httpMethod = method.rawValue

        if let headers = headers 
            urlRequest.allHTTPHeaderFields = headers
            urlRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        

        if let body = body 
            urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: body)
        

        let session = URLSession(configuration: .default)
        let task = session.dataTask(with: urlRequest)  data, response, error in
            completionHandler(data, response, error)
        

        task.resume()
    

好的,所以我想做的是这样的:

apiClient.sendRequest("http://example.com/users/1", ".get")  response in
    switch response 
    case .success(let user):
         print("\(user.firstName)")
    case .failure(let error):
         print(error)
    


apiClient.sendRequest("http://example.com/courses", ".get")  response in
    switch response 
    case .success(let courses):
        for course in courses 
            print("\(course.description")
        
    case .failure(let error):
         print(error)
    

因此,apiClient.sendRequest() 方法必须将响应 json 解码为所需的 swift 对象,并返回该对象或错误对象。

我有这些结构:

struct User: Codable 
    var id: Int
    var firstName: String
    var lastName: String
    var email: String
    var picture: String


struct Course: Codable 
    var id: Int
    var name: String
    var description: String
    var price: Double

我也定义了这个 Result 枚举:

public enum Result<Value> 
    case success(Value)
    case failure(Error)

我被卡住的地方是,我不确定如何在 sendRequest() 中调整我的 completionHandler,以便我可以将它与 User 对象或 Course 对象或任何其他自定义对象一起使用。我知道我必须以某种方式使用泛型来实现这一点,并且我在 C# 中使用过泛型,但我在 Swift 4 中还不是很舒服,所以感谢任何帮助。

编辑:另外,我想知道如何将 sendRequest() 的响应回升到 ViewController 中的调用代码,以便 ViewController 可以访问成功和失败结果(在异步时尚)。

【问题讨论】:

【参考方案1】:

这是一个您可以使用的方法,它将实际的 HTTP 工作转发到现有方法,并且只处理 json 解码:

public func sendRequest<T: Decodable>(for: T.Type = T.self,
                                      url: String,
                                      method: RequestMethod,
                                      headers: HTTPHeaders? = nil,
                                      body: JSON? = nil,
                                      completion: @escaping (Result<T>) -> Void) 

    return sendRequest(url, method: method, headers: headers, body:body)  data, response, error in
        guard let data = data else 
            return completion(.failure(error ?? NSError(domain: "SomeDomain", code: -1, userInfo: nil)))
        
        do 
            let decoder = JSONDecoder()
            try completion(.success(decoder.decode(T.self, from: data)))
         catch let decodingError 
            completion(.failure(decodingError))
        
    

,可以这样调用:

apiClient.sendRequest(for: User.self,
                      url: "https://someserver.com",
                      method: .get,
                      completion:  userResult in
                        print("Result: ", userResult)
)

,或者像这样:

apiClient.sendRequest(url: "https://someserver.com",
                      method: .get,
                      completion:  (userResult: Result<User>) -> Void in
                        print("Result: ", userResult)
)

,通过指定完成签名并省略第一个参数。无论哪种方式,如果我们提供足够的信息,我们让编译器推断其他东西的类型。

在多个方法之间拆分职责使它们更易于重用、更易于维护和理解。

假设您将 api 客户端包装到另一个类中,该类公开了一些更通用的方法,隐藏了 api 客户端的复杂性,并允许通过仅传递相关信息从控制器中调用,您最终可能会得到如下一些方法:

func getUserDetails(userId: Int, completion: @escaping (Result<User>) -> Void) 
    apiClient.sendRequest(for: User.self,
                          url: "http://example.com/users/1",
                          method: .get,
                          completion: completion)

,可以像这样简单地从控制器调用:

getUserDetails(userId: 1)  result in
    switch result 
    case let .success(user):
        // update the UI with the user details
    case let .failure(error):
        // inform about the error
    

更新还可以通过在sendRequest()上添加另一个重载来轻松添加对解码数组的支持,下面是答案开头的代码的一个小的重构版本:

private func sendRequest<T>(url: String,
                            method: RequestMethod,
                            headers: HTTPHeaders? = nil,
                            body: JSON? = nil,
                            completion: @escaping (Result<T>) -> Void,
                            decodingWith decode: @escaping (JSONDecoder, Data) throws -> T) 
    return sendRequest(url, method: method, headers: headers, body:body)  data, response, error in
        guard let data = data else 
            return completion(.failure(error ?? NSError(domain: "SomeDomain", code: -1, userInfo: nil)))
        
        do 
            let decoder = JSONDecoder()
            // asking the custom decoding block to do the work
            try completion(.success(decode(decoder, data)))
         catch let decodingError 
            completion(.failure(decodingError))
        
    


public func sendRequest<T: Decodable>(for: T.Type = T.self,
                                      url: String,
                                      method: RequestMethod,
                                      headers: HTTPHeaders? = nil,
                                      body: JSON? = nil,
                                      completion: @escaping (Result<T>) -> Void) 

    return sendRequest(url: url,
                       method: method,
                       headers: headers,
                       body:body,
                       completion: completion)  decoder, data in try decoder.decode(T.self, from: data) 


public func sendRequest<T: Decodable>(for: [T].Type = [T].self,
                                      url: String,
                                      method: RequestMethod,
                                      headers: HTTPHeaders? = nil,
                                      body: JSON? = nil,
                                      completion: @escaping (Result<[T]>) -> Void) 

    return sendRequest(url: url,
                       method: method,
                       headers: headers,
                       body:body,
                       completion: completion)  decoder, data in try decoder.decode([T].self, from: data) 

现在你也可以这样做:

func getAllCourses(completion: @escaping (Result<[Course]>) -> Void) 
    return apiClient.sendRequest(for: User.self,
                                 url: "http://example.com/courses",
                                 method: .get,
                                 completion: completion)


// called from controller
getAllCourses  result in
    switch result 
    case let .success(courses):
        // update the UI with the received courses
    case let .failure(error):
        // inform about the error
    

【讨论】:

好的,你可以这样称呼它: sendDecodableRequest(...) ?这对一系列课程有何作用? 酷,这行得通。还有一个问题。如果我想将 apiClient.sendRequest 的结果再返回到我的 ViewController,以便 ViewController 可以检查成功或失败(以异步方式),我是否只返回 userResult?或者我可以直接返回整个事情:return apiClient.sendRequest(...) 我想我明白你在说什么,但如果你能记下一个简单的例子,那将有很大帮助!我快到了! 已编辑,感谢您的帮助! 太棒了,谢谢!试图理解这一行:尝试完成(.success(解码(解码器)))。正在寻找自定义方法 decode() 但它似乎不存在?

以上是关于从 REST API 客户端返回一个通用的 swift 对象的主要内容,如果未能解决你的问题,请参考以下文章

从 REST API 返回的图像总是显示损坏

REST API 错误返回良好做法 [关闭]

REST API访问控制从访问令牌和路径参数中提取主题

设计用于返回多个对象计数的 REST api

用于 Android 客户端的 Django Rest Framework 响应的通用 JSON 格式

通过 GraphQL 应用从 REST api 调用返回的 cookie