I'm implementing an API Client that will call my backend API, and return the appropriate object, or an error.
This is what I have so far:
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()
}
}
Ok, so what I want to be able to do is something like this:
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)
}
}
So, the apiClient.sendRequest() method has to decode the response json into the desired swift object, and return either that object or an error object.
I have these structs:
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
}
I have this Result enum defined as well:
public enum Result<Value> {
case success(Value)
case failure(Error)
}
Where I am stuck is, I am not sure how to tweak my completionHandler in sendRequest() so that I can use it with a User object or a Course object or any other custom object. I know I have to use generics somehow to make this happen, and I've used generics in C#, but I'm not quite comfortable yet in Swift 4, so any help is appreciated.
EDIT: Also, I'd like to know how sendRequest()'s response can be bubbled back up one level to the calling code in the ViewController, so that the ViewController has access to the success and failure results (in an async fashion).
Here's a method that you can use, that forwards the actual HTTP work to the existing method, and handles only the json decoding:
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))
}
}
}
, which can be called like this:
apiClient.sendRequest(for: User.self,
url: "https://someserver.com",
method: .get,
completion: { userResult in
print("Result: ", userResult)
})
, or like this:
apiClient.sendRequest(url: "https://someserver.com",
method: .get,
completion: { (userResult: Result<User>) -> Void in
print("Result: ", userResult)
})
, by specifying the completion signature and omitting the first argument. Either way, we let the compiler infer the type for the other stuff, if we provide it enough information to do so.
Splitting the responsibilities between multiple methods makes them more reusable, easier to maintain and understand.
Assuming you wrap the api client into another class that exposes some more general methods, that hide the api client complexity, and allow to be called from controllers by passing only the relevant information, you could end up with some methods like this:
func getUserDetails(userId: Int, completion: @escaping (Result<User>) -> Void) {
apiClient.sendRequest(for: User.self,
url: "http://example.com/users/1",
method: .get,
completion: completion)
}
, which can be simply called from the controller like this:
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
}
}
Update Support for decoding arrays can also be easily added by adding another overload over sendRequest(), below is a small refactored version of the code from the beginning of the answer:
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) }
}
Now you can also do something like this:
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
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With