Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save satishbabariya/8f27c1043230921cefdccdc9b0590abd to your computer and use it in GitHub Desktop.
Save satishbabariya/8f27c1043230921cefdccdc9b0590abd to your computer and use it in GitHub Desktop.
supercharge api calls with codable and generics

supercharge api calls with codable and generics

Codeables and Generics are very powerful parts of Swift. They are pretty, simple, and very powerful!

If you aren't familiar with them, I encourage you to take a look! You can find detailed information about Generics and Codable here. [Generics]https://docs.swift.org/swift-book/LanguageGuide/Generics.html and [Codable]https://developer.apple.com/documentation/swift/codable

Take this example: you have to implement an API in your app. No big deal, right? Almost anybody can do that! Let me add the pod for Alamo... no, we're not taking that route, we're going to implement everything ourselves!

Why? You may ask. API libraries are gigantic, they have tons and tons and tons of functionalities, they make your app bulkier, and they add something in between your code and the server (hey, I trust them, but you cannot deny that you are giving up control).

Still onboard? Let's get this party started.

You can find all the code for this little project at the end of this document in a Github repository.

=========

First we need to write some skeleton code, like HTTPClient protocol and HTTPMethod enum

/// HTTP Methods
public enum HTTPMethod: String {
    case get = "GET"
    case head = "HEAD"
    case post = "POST"
    case put = "PUT"
    case delete = "DELETE"
    case connect = "CONNECT"
    case options = "OPTIONS"
    case trace = "TRACE"
    case patch = "PATCH"
}

/// The protocol used to define the specifications necessary for a `HTTPClient`.
public protocol HTTPClient {
    /// The host, conforming to RFC 1808.
    var host: String { get }

    /// The path, conforming to RFC 1808
    var path: String { get }

    /// API Endpoint
    var endpoint: String { get }

    /// The HTTP method used in the request.
    var method: HTTPMethod { get }

    /// The HTTP request parameters.
    var parameters: [String: Any]? { get }

    /// A dictionary containing all the HTTP header fields
    var headers: [String: String]? { get }
}

You may be asking yourself why do we need this. The answer is simple:

This protocol is a wrapper, it will allow us to decouple the app and the API, which in turn will allow us to mock it easily for testing, have several providers… even change completely the API code without changing a single line in the rest of the app!

Protocols are awesome

we'll use HTTPClient in defining api endpoint where we'll define host, path, endpoint and it's corpsponding http method. we're going to use JSONPlaceholder for api server.

let's create service for {JSON} Placeholder (Free fake API for testing and prototyping.), First, set up an enum with all of your API targets. Note that you can include information as part of your enum. Let's look at a common example. First we create a new file named JSONPlaceholderService.swift

enum JSONPlaceholderService {
   case todos
}

This enum is used to make sure that you provide implementation details for each target (at compile time). You can see that parameters needed for requests can be defined as per the enum cases parameters. The enum must additionally conform to the HTTPClient protocol. Let's get this done via an extension in the same file:

extension JSONPlaceholderService: HTTPClient {
    var host: String {
        return "https://jsonplaceholder.typicode.com/"
    }
    
    var path: String {
        return ""
    }
    
    var endpoint: String {
        switch self {
        case .todos:
            return "todos"
        }
    }
    
    var method: HTTPMethod {
        switch self {
        case .todos:
            return .get
        }
    }
    
    var parameters: [String : Any]? {
        return nil
    }
    
    var headers: [String : String]? {
        return ["Content-type": "application/json"]
    }
}

====================

Let's define enum to handle errors, I cannot recommend enough implementing one in every app, this way we are able to generate strongly typed errors, which are much more secure to handle. As you can see, only a handful of errors are contemplated.

/// HTTPClient Errors
enum HTTPClientError: Error {
    case badURL
}

=======================

now let's write request function, This function needs a Generic type T that conforms to Codable. we'll going to use URLSession insted of Alomafire or Moya.

URLSession is the iOS class responsible for managing network requests. You can make your own session if you want to, but it’s very common to use the shared session that iOS creates for us to use – unless you need some specific behavior, using the shared session is fine.

Our code then calls dataTask(with:) on that shared session, which creates a networking task from a URLRequest and a closure that should be run when the task completes. In our code that’s provided using trailing closure syntax, and as you can see it accepts three parameters:

data is whatever data was returned from the request. response is a description of the data, which might include what type of data it is, how much was sent, whether there was a status code, and more. error is the error that occurred. Now, cunningly some of those properties are mutually exclusive, by which I mean that if an error occurred then data won’t be set, and if data was sent back then error won’t be set. This strange state of affairs exists because the URLSession API was made before Swift came along, so there was no nicer way of representing this either-or state.

Notice the way we call resume() on the task straight away? That’s the gotcha – that’s the thing you’ll forget time and time again. Without it the request does nothing and you’ll be staring at a blank screen. But with it the request starts immediately, and control gets handed over to the system – it will automatically run in the background, and won’t be destroyed even after our method ends.

extension HTTPClient {
    /// The URL of the receiver.
    fileprivate var url: String {
        return host + path + endpoint
    }

    func request<T: Codable>(type: T.Type, completionHandler: @escaping (Result<T, Error>) -> Void) {
        guard let url = URL(string: url) else {
            completionHandler(.failure(HTTPClientError.badURL))
            return
        }

        var request = URLRequest(url: url)
        request.httpMethod = method.rawValue
        request.allHTTPHeaderFields = headers
        request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData

        if let parameters = parameters {
            do {
                request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
            } catch {
                completionHandler(.failure(error))
                return
            }
        }

        let session = URLSession.shared
        let dataTask = session.dataTask(with: request, completionHandler: { (data, _, error) -> Void in
            if let error = error {
                completionHandler(.failure(error))
                return
            }

            if let data = data {
                do {
                    completionHandler(.success(try JSONDecoder().decode(type, from: data)))
                } catch {
                    completionHandler(.failure(error))
                    return
                }
            }
        })

        dataTask.resume()
    }
}

You can see that the HTTPClient protocol makes sure that each value of the enum translates into a full request. Each full request is split up into the host, the path specifying the subpath of the request, the method which defines the HTTP method Note that at this point you have added enough information for a basic API networking layer to work. By default HTTPClient will combine all the given parts into a full request

========

This function is where the magic happens, it creates a dataTask with the previously created request and decodes its response to the type of T.

Remember, the decoder needs to know into which type it should try to decode the JSON, so, how does this work?

First, we ensured that T was Codable, that fulfils the first requirement. What about the second one? How does JSONDecoder know into which type it must try to decode the JSON?

Remember the protocol? When we defined one function for each endpoint?

The Swift compiler is smart enough to infer that when calling this function from each of those points, the type of T will be the type defined in the function of the protocol. It’s an awesome compiler!

Now Let's do some API Calls

first we'll define Todo Models that confirms Codable Protocols

struct Todo: Codable {
    let userID, id: Int
    let title: String
    let completed: Bool

    enum CodingKeys: String, CodingKey {
        case userID = "userId"
        case id, title, completed
    }
}

Now how do we make a request?

JSONPlaceholderService.todos.request(type: [Todo].self) { result in
    switch result {
    case .success(let todos):
        break
    case .failure(let error):
        break
    }
}

request will wrap the success or failure in a Result enum. result is either .success(Codable Class Given by user) or .failure(Error).

Take special note: a .failure means that the server either didn’t receive the request (e.g. reachability/connectivity error) or it didn’t send a response (e.g. the request timed out). If you get a .failure, you probably want to re-send the request after a time delay or when an internet connection is established.

@satishbabariya
Copy link
Author

satishbabariya commented Apr 4, 2022

@dipsvaishnav you can use parameters enum like this,

enum JSONPlaceholderService {
   case todo(id: Int)
}

and as for consuming params in api you can do something like this

    var parameters: [String : Any]? {
       switch self {
        case .todos(let id):
            return ["id": id]
        default:
            return nil
        }
    }

@ankuhgupta646
Copy link

how to pass multiple parameter with post request please share code

@nisarg-sit
Copy link

@ankuhgupta646 For multiple params

enum JSONPlaceholderService {
   case todo(Int,String)
}

and as for consuming params in api you can do something like this

 var parameters: [String : Any]? {
       switch self {
        case .todos(let id, let title):
            return ["id": id, "title": title]
        default:
            return nil
        }
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment