Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Return a generic swift object from a REST API Client

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).

like image 500
Prabhu Avatar asked Jan 18 '26 12:01

Prabhu


1 Answers

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
    }
}
like image 166
Cristik Avatar answered Jan 20 '26 11:01

Cristik



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!