Network Layer in iOS (1/4)

Abhishek Ravi 🇮🇳
10 min readApr 1, 2023

Objective

Network Layer is one of the most common reusable component of mobile app. Some of the question, we always keep in mind, while writing the network layer is — It should be loosely coupled with the response type & network library should not be exposed to outside of the class, so that we’re not dependent on the network library.

Agenda

So, this series is going to be in four different parts, I don’t want to bundle each and everything under a single article —

Part 1 — REST APIs Task

  1. URLSession
  2. We will write a GET and POST method and consume APIs (considering Protocol Oriented Programming)
  3. Refactor the method and make it type loosely coupled using Generics ✌️
  4. Re-Write the complete class with Alamofire

Part 2— Download and Upload

  1. Download Task
  2. Upload Task
  3. Refactor above methods to make it more reusable and under POP
  4. Re-write same task in Alamofire

Part 3— Alamofire Features

  1. Reachability
  2. Retry Mechanism on Failure
  3. Use Middleware and Interceptor
  4. Log Response and Metric in Firebase Performance
  5. Handle Error Code efficiently

Part 4— UnitTest and Framework

  1. Write Test Case of this module
  2. Convert it into reusable module
  3. Publish it in Cocopods

URLSession

URLSession is part of Foundation framework which is used to do network request. It supports majorly three types of tasks —

  1. Data Task — GET, POST, PUT, DELETE requests
  2. Download Task— Download resource from internet
  3. Upload Task — Upload a resource to internet

Before writing code for these task, you always bother about two more thing. One is URLSession object and other is URLSessionConfiguration.

URLSessionConfiguration is a dependency for URLSession, first you will initialise the URLSessionConfiguration Object and then inject it in URLSession. And, when you are ready with URLSession Object then, you can execute any of the task — data task, download task or upload task.

import Foundation

final class URLSessionApiClient {

private let configuration: URLSessionConfiguration

init() {
self.configuration = URLSessionConfiguration.default
}

}

From here, you are good to go and write the session code. But, we will explore little bit URLSessionConfiguration’s property and different types —

  1. URLSessionConfiguration.default — just default one
  2. URLSessionConfiguration.ephemeral — uses no persistence storage
  3. URLSessionConfiguration.background(withIdentifier: “app.example.name”) — enables background support

And, every configuration has some setter property —

self.configuration = URLSessionConfiguration.default
self.configuration.timeoutIntervalForRequest = 30.0
self.configuration.httpAdditionalHeaders = ["Content-Type": "application/json"]

You can go through the documentation, if you want to explore more. I just want you to know, if you want to change any behaviour in your HTTP request, always check configuration object.

Now, when you have the URLSessionConfiguration object, then you are ready to create URLSession object (since, URLSessionConfiguration is a dependency for URLSession).

final class URLSessionApiClient {

private let configuration: URLSessionConfiguration
private let session: URLSession

init() {
self.configuration = URLSessionConfiguration.default
self.configuration.timeoutIntervalForRequest = 30.0
self.configuration.httpAdditionalHeaders = ["Content-Type": "application/json"]

// Create URLSession
self.session = URLSession(configuration: self.configuration)
}
}

Since, we have URLSessionConfiguration object, we can create our own session object.

And, if you feel like it’s a too much of work then URLSession has given you a default session — URLSession.shared

DataTask with GET

This task represents HTTP request which you will need to consume any REST APIs. It’s an asynchronous method, which will return Data, URLResponse and Error object. Let’s write a dataTask method which will look something like this —

final class URLSessionApiClient {

private let configuration: URLSessionConfiguration
private let session: URLSession

init() {
self.configuration = URLSessionConfiguration.default
self.configuration.timeoutIntervalForRequest = 30.0
self.configuration.httpAdditionalHeaders = ["Content-Type": "application/json"]

self.session = URLSession(configuration: self.configuration)
}

func dataTask(_ url: URL) {
let urlRequest = URLRequest(url: url)
self.session.dataTask(with: urlRequest) { data, response, error in
//TODO: Datatask Callback
print("HTTP Data:\(data)")
}.resume()
}

}

Here, dataTask(_:) always expects URLRequest type, so we need to create one using url.

import UIKit

class ViewController: UIViewController {

var client = URLSessionApiClient()

override func viewDidLoad() {
super.viewDidLoad()

let apiURL = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!
client.dataTask(apiURL)
}
}

In console, you will get the output something like this —

Since, it’s going to be an asynchronous method. We need to add the completion handler in the dataTask method’s signature —

func dataTask(_ url: URL, onCompletion: @escaping (_ result: Result<Data, Error>) -> Void) {
let urlRequest = URLRequest(url: url)
self.session.dataTask(with: urlRequest) { data, response, error in

// onFailure
if let err = error {
onCompletion(.failure(err))
return
}

// onSuccess
if let data = data {
onCompletion(.success(data))
} else {
onCompletion(.failure(AppError.noHttpBody))
}
}.resume()
}

Here, we have handled the failure and success case and pass it to the completion handler with Result<Data, Error> type.

override func viewDidLoad() {
super.viewDidLoad()

let apiURL = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!
client.dataTask(apiURL) { result in
switch result {
case .failure(let error):
print(error)
case .success(let data):
print("Data: \(data)")
}
}
}

Till now, it is good and we are getting Data type in the response, let’s try to make it more beautiful by incorporating Codable type in the response. So, we’ll write a Codable class for https://jsonplaceholder.typicode.com/posts/1 API.

{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}

And, Codable Class for this JSON will be like —

struct PostModel: Codable {
let userId: Int
let id: Int
let title: String
let body: String
}

Now, we will refactor our dataTask method in URLSessionApiClient. Change the completionHandler(_ result: Result<Data, Error>) to completionHandler(_ result: Result<PostModel, Error>).

func dataTask(_ url: URL, onCompletion: @escaping (_ result: Result<PostModel, Error>) -> Void) {
let urlRequest = URLRequest(url: url)
self.session.dataTask(with: urlRequest) { data, response, error in

// onFailure
if let err = error {
onCompletion(.failure(err))
return
}

// onSuccess
if let data = data {
// Transform Data to Codable Type
if let userModel = try? JSONDecoder().decode(PosrModel.self, from: data) {
onCompletion(.success(userModel))
} else {
onCompletion(.failure(AppError.decodingError))
}
} else {
onCompletion(.failure(AppError.noHttpBody))
}
}.resume()
}

Note: we are using JSONDecoder class which is used to convert Data to Codable Type. This can be different article for JSONDecoder and JSONEncoder.

This is great, but in dataTask method, we have tightly coupled with the PostModel class. Since, we will be using this URLSessionApiClient across the app. So, we can’t make dataTask method with PostModel.

Let’s make it loosely coupled with any Codable Type. Yes, we are going to intrdouce Generics —

 func dataTask<T: Codable>(_ url: URL, onCompletion: @escaping (_ result: Result<T, Error>) -> Void) {
let urlRequest = URLRequest(url: url)
self.session.dataTask(with: urlRequest) { data, response, error in

// onFailure
if let err = error {
onCompletion(.failure(err))
return
}

// onSuccess
if let data = data {
// Transform Data to Codable Type
if let userModel = try? JSONDecoder().decode(T.self, from: data) {
onCompletion(.success(userModel))
} else {
onCompletion(.failure(AppError.decodingError))
}
} else {
onCompletion(.failure(AppError.noHttpBody))
}
}.resume()
}

And, whoever is going to consume this dataTask method, they need to mention the Codable type —

override func viewDidLoad() {
super.viewDidLoad()

let apiURL = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!
client.dataTask(apiURL) { (_ result: Result<PostModel, Error>) in
switch result {
case .failure(let error):
print(error)
case .success(let data):
print("Data: \(data)")
}
}
}

Let’s verify, if our dataTask(_:) is working properly for other APIs response — https://jsonplaceholder.typicode.com/users/1

{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
}
}

And, it’s Codable Class will be like —

struct UserModel: Codable {
let id: Int
let name: String
let username: String
let email: String
let address: UserAddress
}

struct UserAddress: Codable {
let street: String
let suite: String
let city: String
let zipcode: String
}

Let’s try to consume /users API.

class ViewController: UIViewController {

var client = URLSessionApiClient()

override func viewDidLoad() {
super.viewDidLoad()

let userAPIURL = URL(string: "https://jsonplaceholder.typicode.com/users/1")!
client.dataTask(userAPIURL) { (_ result: Result<UserModel, Error>) in
switch result {
case .failure(let error):
print(error)
case .success(let data):
print("Data: \(data)")
}
}
}
}

Yes, it is working without any change in API Client ✌️🚀

DataTask with POST

We will improve our dataTask() method, instead of sending just URL in param, we need to introduce other param which is necessary for any HTTP. Those parameters are —

  1. API URL
  2. HTTP Method
  3. Custom Headers
  4. Query Params in URL
  5. HTTP Body

So let’s write a APIRequest struct for this which will have all this.

enum HTTPMethod: String {
case GET
case POST
case PUT
case DELETE
}

struct APIRequest {
let url: URL
let method: HTTPMethod
let headers: [String: String]?
let queryParams: [String: Any]?
let body: Data?
}

Now, we need to refactor the dataTask method which accepts APIRequest instead of just URL and incorporate these property in URLSession’s request.

 private func prepareURL(_ api: APIRequest) -> URL? {
var urlComponents = URLComponents(string: api.url.absoluteString)
let queryItems = api.queryParams?.map({ (key, value) in
return URLQueryItem(name: key, value: String(describing: value) )
})
urlComponents?.queryItems = queryItems
return urlComponents?.url
}

func dataTask<T: Codable>(_ api: APIRequest, onCompletion: @escaping (_ result: Result<T, Error>) -> Void) {

guard let url = prepareURL(api) else {
return onCompletion(.failure(AppError.invalidURL))
}

var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = api.method.rawValue
urlRequest.allHTTPHeaderFields = api.headers
urlRequest.httpBody = api.body

self.session.dataTask(with: urlRequest) { data, response, error in

// onFailure
if let err = error {
onCompletion(.failure(err))
return
}

// onSuccess
if let data = data {
// Transform Data to Codable Type
if let userModel = try? JSONDecoder().decode(T.self, from: data) {
onCompletion(.success(userModel))
} else {
onCompletion(.failure(AppError.decodingError))
}
} else {
onCompletion(.failure(AppError.noHttpBody))
}
}.resume()
}

Here, I have created an extra method prepareURL, which will create URL with query Params. Responsibility of prepare URL is to prepare final URL by appending query params in the URL.

URL = https://greensyntax.app
Query Params = ["firstName": "Abhishek", lastName: "Ravi"]

// This is what prepareURL() method is going to do with Query Params
Final URL = https://greensyntax.app?firstName=Abhishek&lastName=Ravi

Let’s consume GET /users APIs —

 override func viewDidLoad() {
super.viewDidLoad()

let userAPI = URL(string: "https://jsonplaceholder.typicode.com/users/1")!
let apiRequest = APIRequest(url: userAPI, method: .GET, headers: nil, queryParams: nil, body: nil)
client.dataTask(apiRequest) { (_ result: Result<UserModel, Error>) in
switch result {
case .failure(let error):
print(error)
case .success(let data):
print("Data: \(data)")
}
}
}

Now, let’s try to consumer POST /posts API where we need to pass the body and headers (let’s assume).

    override func viewDidLoad() {
super.viewDidLoad()

let newPost = PostModel(userId: 1234, id: 1234, title: "My Title", body: "This is Body")
let newPostData = try? JSONEncoder().encode(newPost)

let postsAPI = URL(string: "https://jsonplaceholder.typicode.com/posts")!
let apiRequest = APIRequest(url: postsAPI, method: .POST, headers: ["Content-Type":"application/json"], queryParams: nil, body: newPostData)
client.dataTask(apiRequest) { (_ result: Result<PostModel, Error>) in
switch result {
case .failure(let error):
print(error)
case .success(let data):
print("Data: \(data)")
}
}
}

And, if everything went well, you will get status code as `201` and response body would be something like this —

Last thing, I just want to make a small change in API Client. I want to introduce HTTP Status Code Validation Check.

func dataTask<T: Codable>(_ api: APIRequest, onCompletion: @escaping (_ result: Result<T, Error>) -> Void) {

guard let url = prepareURL(api) else {
return onCompletion(.failure(AppError.invalidURL))
}

var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = api.method.rawValue
urlRequest.allHTTPHeaderFields = api.headers
urlRequest.httpBody = api.body

self.session.dataTask(with: urlRequest) { data, response, error in

// onFailure
if let err = error {
onCompletion(.failure(err))
return
}

// Validation
guard (200...299).contains((response as? HTTPURLResponse)?.statusCode ?? 0) else {
onCompletion(.failure(AppError.httpFailure))
return
}

// onSuccess
if let data = data {
// Transform Data to Codable Type
if let userModel = try? JSONDecoder().decode(T.self, from: data) {
onCompletion(.success(userModel))
} else {
onCompletion(.failure(AppError.decodingError))
}
} else {
onCompletion(.failure(AppError.noHttpBody))
}
}.resume()
}

Introduce Alamofire

Alamofire is a popular network library for iOS, you can use Alamofire instead of URLSession. You can go through Alamofire’s github page for other features they have. Right now, we are just bothered about the HTTP Data Request and consume GET and POST APIs via Alamofire.

# Open Project
# Create Pod file and open it
# Add pod 'Almofire'
# Run pod install command

So, I will try to just re-write the same implementation using Alamofire. Codes are self explanatory, if you get the URLSession then, Alamofire is just an abstraction over it.

import Foundation
import Alamofire

final class AlamofireApiClient: NetworkClient {

private let session = AF.session

private func prepareURL(_ api: APIRequest) -> URL? {
var urlComponents = URLComponents(string: api.url.absoluteString)
let queryItems = api.queryParams?.map({ (key, value) in
return URLQueryItem(name: key, value: String(describing: value) )
})
urlComponents?.queryItems = queryItems
return urlComponents?.url
}

func dataTask<T>(_ api: APIRequest, onCompletion: @escaping (Result<T, Error>) -> Void) where T : Decodable, T : Encodable {

guard let url = prepareURL(api) else {
return onCompletion(.failure(AppError.invalidURL))
}

var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = api.method.rawValue
urlRequest.allHTTPHeaderFields = api.headers
urlRequest.httpBody = api.body

AF.request(urlRequest).validate().response { response in

// onFailure
if let err = response.error {
onCompletion(.failure(err))
return
}

// onSuccess
if let data = response.data {
// Transform Data to Codable Type
if let userModel = try? JSONDecoder().decode(T.self, from: data) {
onCompletion(.success(userModel))
} else {
onCompletion(.failure(AppError.decodingError))
}
} else {
onCompletion(.failure(AppError.noHttpBody))
}
}
}

}

Here, NetworkClient is a protocol for both the Clients to keep the contract remain same.

protocol NetworkClient {
func dataTask<T: Codable>(_ api: APIRequest,
onCompletion: @escaping (_ result: Result<T, Error>) -> Void)
}

Response for the both the GET and POST APIs which we have tested for URLSession.

🌎 Source Code

Github: https://github.com/greenSyntax/network-layer
Branch : feature/alamofire and main

Conclusion

Here, we gone through the process how we should design a Network Layer. Majorly, we discussed GET and POST APIs with URLSession and Alamofire. I usually prefer URLSession in my projects if I am just consuming GET and POST APIs, but if has has caching, logging, metric analysis, upload & download then, I will obviously go for Alamofire as, it’s a mature library which is relevant from a long time.

--

--