iOS Developer: From Noob to Ninja in 30 days — Day 6, Network layer, don’t make this mistake, use Generics!!!

In this post I will be using Generics to refactor a common mistake creating a Network layer, don’t do this on your code.

Photo by @zgc1993

Before I start talking about the Network layer let's talk a little bit about generics.

So what does Apple say about generics?

Generic code enables you to write flexible, reusable functions and types that can work with any type, subject to requirements that you define. You can write code that avoids duplication and expresses its intent in a clear, abstracted manner.Generics in Network Request

My own definition of generics would be:

A generic it is just a template that can be used by any type and with that avoid repeated code.

Now we will work on the Network layer and improve it with generics.

Usually when your app consumes API services most of the developers (newbies) use Alamofire or more adventure ones create their own Network layer.

I would not say you don’t use Alamofire, but before use that dependency think about whether you really need all this code in your project.

Alamofire files

I will be talking about this in another post, so for now, we will create our own Network layer.

To make it more readable, we will use a typealias for our Result type, like I show you below.

typealias NetworkResult<T: Codable> = Result<T, NetworkError>

If you want to know more about typealias you can follow the link below.

Back to our Network layer a common mistake when you are working with requests is create many methods as you need and repeat code in each one, you can see that in the code below.

struct Network {
func getCryptos(networkRequest: NetworkEndPoint,
completion: @escaping (NetworkResult<[Crypto]>) -> Void)
{
let request = networkRequest.urlRequestlet task = networkRequest.session.dataTask(with: request) { (data, urlResponse, error) in

let
result: NetworkResult<[Crypto]>

defer {
completion(result)
}
guard let data = data else {
return result = .failure(.data)
}

do {
let objectDecoded = try networkRequest.jsonDecoder.decode([Crypto].self, from: data)
result = .success(objectDecoded)
} catch {

result = .failure(.decoding(error: error))
}
}
task.resume() }func createPost(networkRequest: NetworkEndPoint,
httpBody: Data? = nil,
completion: @escaping (NetworkResult<PostResponse>) -> Void)
{
var request = networkRequest.urlRequest
request.httpBody = httpBody
let task = networkRequest.session.dataTask(with: request) { (data, urlResponse, error) in

let
result: NetworkResult<PostResponse>
defer {
completion(result)
}
guard let data = data else {
return result = .failure(.data)
}
do { let objectDecoded = try networkRequest.jsonDecoder.decode(PostResponse.self, from: data)
result = .success(objectDecoded)
} catch {
result = .failure(.decoding(error: error))
}
}
task.resume() }
}

Maybe you can tell me: I did not see anything wrong there.

Yes I know it is hard to see what is happening above, I show you in the image below the code the lines that are doing the same task with different types and that code can be refactored using Generics.

Remember always apply the DRY (don’t repeat your self) principle in your code.

In general the whole body in these methods are almost the same, but there are some important parts that I explain you below.

  1. Completion handler (closure), in these lines you can see two closures
completion: @escaping (NetworkResult<[Crypto]>) -> Void
completion: @escaping (NetworkResult<PostResponse>) -> Void)

The main difference between these methods is the type used.

We can change this by using a generic as I show you below where T is equals to the generic type.

completion: @escaping (NetworkResult<T>) -> Void

If you need to remember a little bit about closures the link below can help you.

2. These lines define a result type constants.

let result: NetworkResult<[Crypto]>
let result: NetworkResult<PostResponse>

We can change this by using a generic T as I show you below.

let result: NetworkResult<T>

3. These lines define the decoding process with Codables.

let objectDecoded = try networkRequest.jsonDecoder.decode([Crypto].self, from: data)let objectDecoded = try networkRequest.jsonDecoder.decode(PostResponse.self, from: data)

We can change this by using a generic T as I show you below.

let objectDecoded = try networkRequest.jsonDecoder.decode(T.self, from: data)

If you want to know more about Codables in swift you can follow the link below.

Now take a look at these methods, they are almost the same as well.

func getCryptos(networkRequest: NetworkEndPoint,
completion: @escaping (NetworkResult<[Crypto]>) -> Void)
func createPost(networkRequest: NetworkEndPoint,
httpBody: Data? = nil,
completion: @escaping (NetworkResult<PostResponse>) -> Void)

We can create a method replacing the types using generics, like I show you below.

func fetch<T: Codable>(networkRequest: NetworkEndPoint,
httpBody: Data? = nil,
completion: @escaping (NetworkResult<T>) -> Void)

After applied those changes we can create a method using generics like I show you below.

func fetch<T: Codable>(networkRequest: NetworkEndPoint,
httpBody: Data? = nil,
completion: @escaping (NetworkResult<T>) -> Void) { // 1
var request = networkRequest.urlRequest
request.httpBody = httpBody
let task = networkRequest.session.dataTask(with: request) { (data, urlResponse, error) in // 2 let result: NetworkResult<T> defer {
completion(result)
}

guard let data = data else {
return result = .failure(.data)
}
result = decodeData(networkRequest: networkRequest,
data: data) // 3
}
task.resume()
}
private func decodeData<T: Codable>(networkRequest: NetworkEndPoint,
data: Data) -> NetworkResult<T> {
do {
let objectDecoded = try networkRequest.jsonDecoder.decode(T.self, from: data)
return .success(objectDecoded)
} catch {

return .failure(.decoding(error: error))
}
}

Let’s explain that code.

  1. These lines create a generic method with three parameters.
  • The parameter networkRequest defines the end point used for create the request.
networkRequest: NetworkEndPoint

The NetworkEndPoint is defined by a protocol with default values as I show you below.

protocol NetworkEndPoint {  var baseURL: URL { get }
var network: Network { get }
var jsonDecoder: JSONDecoder { get }
var urlRequest: URLRequest { get }
}
  • The parameter httpBody defines the Data used in the body request.
httpBody: Data? = nil
  • The completion handler defines a NetworkResult type.
completion: @escaping (NetworkResult<T>) -> Void

Where NetworkResult is a generic typealias.

2. This line creates a task and retrieves the data from the URL set.

If you want to know more about URLSession you can follow the link below.

3. This line creates a call to the decodeData method, that method decodes the data received in the request process also uses generics such as the fetch method.

Now we are ready for use our generic method in CryptosEndPoint and CreatePostEndPoint.

CryptosEndPoint and getCryptos request

This endpoint provides an array of Crypto instances.

struct CryptosEndPoint: NetworkEndPoint {  var urlRequest: URLRequest {
let request = URLRequest.getRequest(url: baseURL,
httpMethod: .get,
pathComponent: CryptoPath.cryptos)
return request
}
func getCryptos(completion: @escaping (NetworkResult<[Crypto]>) -> Void) { network.fetch(networkRequest: self, completion: completion) }
}
let cryptosEndPoint = CryptosEndPoint()cryptosEndPoint.getCryptos { (result) in
switch
result {
case .success(let cryptos):
print(cryptos)
/*
[__lldb_expr_1.Crypto(symbol: "XRP", price: 0.51712048871858), __lldb_expr_1.Crypto(symbol: "WOZX", price: 2.86325750658067), __lldb_expr_1.Crypto(symbol: "BTC", price: 19190.426010045157), __lldb_expr_1.Crypto(symbol: "BTCV", price: 66.61105369058797), __lldb_expr_1.Crypto(symbol: "ALLBI", price: 0.00495452304336), __lldb_expr_1.Crypto(symbol: "DF", price: 0.22036849849893), __lldb_expr_1.Crypto(symbol: "ETH", price: 591.4187273438921), __lldb_expr_1.Crypto(symbol: "TRX", price: 0.02937642050303), __lldb_expr_1.Crypto(symbol: "LBC", price: 0.07171163356022), __lldb_expr_1.Crypto(symbol: "ADA", price: 0.15430348473841)]*/ case .failure(let error):
print(error)
}
}

The image below shows the data used in this request.

Response from cryptos endpoint

CreatePostEndPoint and createPost request

This method sends a json encoding PostData struct and retrieves a PostResponse.

struct PostResponse: Codable {
let status: [String: String]

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
status = try container.decode([String: String].self)
}
}
struct PostData: Codable {
let name: String
let text: String
}
struct CreatePostEndPoint: NetworkEndPoint {
var urlRequest: URLRequest {
let request = URLRequest.getRequest(url: baseURL,
httpMethod: .post,
pathComponent: CryptoPath.createpost)
return request
}
func createPost(_ post: PostData,
completion: @escaping (NetworkResult<PostResponse>) -> Void) {

let httpBody: Data?
do {
httpBody = try JSONEncoder().encode(post)
// httpBody: {"name":"Tony Trejo","text":"New post!!!"}
} catch {
return completion(.failure(.encoding(error: error)))
}
network.fetch(networkRequest: self,
httpBody: httpBody,
completion: completion)
}
}

let
createPostEndPoint = CreatePostEndPoint()
let post = PostData(name: "Tony Trejo", text: "New post!!!")
createPostEndPoint.createPost(post) { (result) in
switch
result {
case .success(let response):
print(response)
// ["status": "Awesome!"]
case .failure(let error):
print(error)
}
}

The image below shows the data used in this request.

Request body and response from createpost endpoint

Additional Resources

  • URLSession
  • Result type
  • Defer
  • Source code

Well we finished this post, hope you now have an idea where to apply the generics and also don’t make that mistake in your Network layer, hope you enjoyed this post and see you in the next post.

Steve jobs: "Sometimes when you innovate, you make mistakes. It is best to admit them quickly, and get on with improving your other innovations".

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store

Tony Trejo

Experienced Software Engineer with 12+ years of experience. https://www.linkedin.com/in/antoniotrejof/