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

普拉布

我正在实现一个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
}

我也定义了这个结果枚举:

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

卡住的地方是,我不确定如何在sendRequest()中调整我的completedHandler,以便可以将其与User对象或Course对象或任何其他自定义对象一起使用。我知道我必须以某种方式使用泛型来实现此目的,并且我已经在C#中使用了泛型,但是在Swift 4中我还不太满意,因此不胜感激。

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

克里斯蒂克

这是一种可以使用的方法,它将实际的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
    }
}

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章