Codable, URLRequest, Result type. These are the main terms that sounds familiar whenever you deal with network in Swift. Networking has always been an important part of every mobile app, thus a well designed network layer is key to a successful app.
Usually when we refer to modularity, network layer works best when it is implemented as a separated module that behaves as the gate for your app to the outer world.
As a rule of thumb, a network module’s main responsibility is to send a request to an external resource, receive the response and give it back to the caller module.
The network module itself can be break down into several components. In this post, we will walkthrough all the parts that contribute to building a networking layer.
HTTP method
This is what specify the HTTP request method, the most common verbs are GET, POST, PUT, PATCH, and DELETE. We can wrap this up in some enum type object. Something like this:
enum HTTPMethod: String { case GET = "GET" case POST = "POST" case DELETE = "DELETE" case PUT = "PUT" case PATCH = "PATCH" func callAsFunction() -> String { rawValue } }
The callAsFunction
allows to apply a function call syntax on the HTTPMethod enum type to return the raw value.
Routes
Route is part of the URL and represents the path we use to access the endpoints. As done with HTTP methods, it is a good practise to group all routes in one place.
enum Routes: String { case posts = "/posts" case comments = "/comments" case albums = "/albums" case photos = "/photos" func callAsFunction() -> String { rawValue } }
HTTP errors
As the request may fail, error handling is a crucial part in a networking layer. A simple request that fetch and decode data from the network may fail for the following reasons:
enum HTTPError: Error { case failedResponse case failedDecoding case invalidUrl case invalidData }
The names above are self explanatory, but they will make more sense when we finish drawing the next parts of the layer 🙂
Scheme and host
The scheme and host are what identify the protocol and the resource name respectively. In the case of https://www.google.com
the scheme is the https
while the host is what comes after. The host can be longer if it is a subdomain – In general, it is what comes between the scheme and the resource path.
For clarity reasons, we can encapsulate these two informations in a struct type, like below:
struct APIConfig { let scheme: String let host: String }
The HTTP Client
The main components of a request are explored above, now is the part where to build a simple client that will take the request, send it over to the network and then send back the received response.
Imagine, we have an app that deals with posts – Few actions we can think about are the following:
- GET posts list
- GET authors list
- GET post details
If we gonna implement each of the following requests, we can think of 3 methods, one for each action:
GET posts list:
func request(request: URLRequest, completion: @escaping (Result<[Post], HTTPError>) -> Void)
GET authors list:
func request(request: URLRequest, completion: @escaping (Result<[Author], HTTPError>) -> Void)
GET post details:
func request(request: URLRequest, completion: @escaping (Result<Post, HTTPError>) -> Void)
All the above methods signature is pretty much the same, except the return type that changes – As you may think, the methods will also have almost the same implementation. As a result we will have duplicated code that does the same think: Send a request and parse a response.
So why not we prevent this code duplication?
Remember in a previous article we used generics to dequeue table view cells – And Generics come to the rescue again. Assuming that all types (Post and Author) conforms to the Decodable protocol. Here is our Post model for instance:
struct Post: Decodable { let title: String let body: String }
As a result, all the methods above can be replaced with one single method, its signature can be as follow:
func request<T: Decodable>(request: URLRequest, completion: @escaping (Result<T, HTTPError>) -> Void)
The type placeholder here is a Decodable. The implementation can be unified as follow:
func request<T: Decodable>(request: URLRequest, completion: @escaping (Result<T, HTTPError>) -> Void) { URLSession.shared.dataTask(with: request) { data, urlResponse, error in // 1 check the response guard let urlResponse = urlResponse as? HTTPURLResponse, (200...299).contains(urlResponse.statusCode) else { completion(.failure(.failedResponse)) return } // 2 check the data guard let data = data else { completion(.failure(.invalidData)) return } // 3 Decode the data do { let decodedData = try JSONDecoder().decode(T.self, from: data) completion(.success(decodedData)) } catch { completion(.failure(.failedDecoding)) } }.resume() }
As you can see, we now have a method that sends ALL types of requests and parses ALL types of data as long as this data conforms to the Decodable protocol. Super handy, no code duplication and a straightforward method.
You may also notice that we started using the HTTP errors defined earlier as part of the failure completion callbacks.
The final piece
The network layer is pretty much done, our app needs some sort of interface to interact with it in order to hit the network.
We can define a class that acts as the point of contact between the network layer and the rest of modules in our app – Consider the following:
class API { private let apiConfig: APIConfig private let apiFetcher: HTTPClient init(apiConfig: APIConfig, apiFetcher: HTTPClient) { self.apiConfig = apiConfig self.apiFetcher = apiFetcher } } extension API { func getPosts(completion: @escaping (Result<[Post],HTTPError>) -> Void) { var components = URLComponents() components.scheme = apiConfig.scheme components.host = apiConfig.host components.path = Routes.posts.callAsFunction() guard let url = components.url else { completion(.failure(.invalidUrl)) return } var urlRequest = URLRequest(url: url) urlRequest.httpMethod = HTTPMethod.GET.callAsFunction() apiFetcher.request(request: urlRequest) { result in completion(result) } } }
The class definition is pretty basic – It has two dependencies in order to instantiate an object: APIConfig and HTTPClient types dependencies.
The getPosts(completion:) method will be called to fetch posts from the endpoint and return an array of Post objects. This method is responsible of building up the request. In this example the request is a simple GET request but in some cases it may require some path and/or query parameter(s). In such cases, this method is the right place to append those parts to the request.
Wrap it up
That’s pretty much it. You now have a complete basic functional networking layer that is easily scalable to implement more tasks.
A simple usage example of our layer would be something like this:
let apiConfig = APIConfig(scheme: "https", host: "jsonplaceholder.typicode.com") let apiFetcher = APIFetcher() let api = API(apiConfig: apiConfig, apiFetcher: apiFetcher) api.getPosts { result in switch result { case .success(let postsResponse): // Display the posts self.posts = postsResponse case .failure(let error): // Handle the error accordingly // Display an error alert print(error.localizedDescription) } }
The above code can be put in a UIViewController and called in the viewDidLoad.
Always remember to separate your network layer in an isolated component – Whether it is a framework or a simple class. The point is, make it clear to you and your fellow peers what will this layer task be among your app modules with a clear interface and a single responsibility approach.
Here is a link to download the sample project.
What are the patterns that you usually follow when building your networking layer? Let me know on Twitter.
Thanks!