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!
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!
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
}
}
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.
@ankuhgupta646 For multiple params
and as for consuming params in api you can do something like this