Highly Customizable Network layer in Swift 5

There are a lot of articles on the web and they are quite useful. What I want to teach using this article is the simplicity, customizability, and modularity of the network layer. My approach is tried and tested in many projects and I have been updating my network layer to incorporate the problems that I faced during the development.


Golden Rules. Before you start reading this article
  1. View controllers should never know about the networking APIs. They shouldn’t even care whether the data is coming from the API or the local database e.g. Coredata or SQLite.
  2. The only layer that should call the networking API is the Service layer.
  3. The view controllers should ask the Service to provide the data.
  4. The Service should not know about the API server and the format of the data that is sent/received to/from the API server.
  5. Only Network Layer should know these details.


What this blog will cover?
  1. Prepare an API Request: <ApiRequest>
  2. Calling an API: <Alamofire>
  3. Parse Response: <Codable>


1. Prepare a Request

Every network request is identified by certain properties i.e.

  1. Endpoint: The path to the network API
  2. Http method type: GET, POST, PUT, DELETE, PATCH, etc
  3. Request parameters: JSON or a string
  4. Response type: JSONXML or a string


End Point

Let’s take an example of an API that returns the stories by a user name get-stories-by-username

// GET All stories by username
https:www.medium.com/api/v1.0/stories/waseem_khan
// server-url/api-path/api-version/resource-name/end-point
// server-url    -> https:www.medium.com/
// api-path      -> api/
// api-version   -> v1.0/
// resource-name -> stories/
// end-point     -> waseem_khan

Most of the times the URL to any API is in the format I mentioned. Usually, the server-url remains the same but the rest of the properties are subject to change per API. Our network layer should be written in a way that supports the customization of each API request.

Let's encapsulate our network request in a swift class.

class ApiRequest<T: Codable> {

 func serverUrl() -> String {
   #if DEBUG
     return https:www.dev.medium.com/
   #else
     return https:www.medium.com/
   #endif
 }

 func apiPath() -> String {
   return "api/"
 }

 func apiVersion() -> String {
   return "v1.0/"
 }

 func apiResource() -> String {
   return ""
 }

 func endPoint() -> String {
   return ""
 }

 func bodyParams() -> NSDictionary? 
   return [:]
 }

 func requestType() -> HTTPMethod {
   return .post
 }

 func contentType() -> String {
   return "application/json"
 }

}

ApiRequest class is the parent for all the API requests that we will have in our project i.e. we will create a separate swift class for every API e.g. LoginApiRequestSignupApiRequestGetStoriesApiRequest. Every request can be customized completely by overriding needed methods from the parent class.

Http method type

Each API can be one of these types i.e GET, POST, PUT, DELETE, PATCH. HTTPMethod enum represents the type of API request

public enum HTTPMethod: String {
case get     = "GET"
case post    = "POST"
case put     = "PUT"
case patch   = "PATCH"
case delete  = "DELETE"
}
// these are mostly used HttpMethod types. You can add more types e.g. TRANE, CONNECT, depending on your need

Request parameters

Depending on the type of the API, you may need to set the request parameters in the body. E.g. LoginRequest can expect request parameters in the below format

{
  "email": "[email protected]",
  "password": "mission#impossible"
}

These parameters can be provided by overriding function bodyParams() in the LoginRequest from the base APIRequest class.

Response type

In the ApiRequest class <T: Codable>, generic T of type Codable is to provide the Codable model object to map the response returned from the API. We can pass any object that is implementing Codable interface here. We can directly map the response from the API to one of our model class. e.g get-stories-by-username API returns the data in below format. We are concerned about the response in the “data” value.

// REQUEST
// GET All stories by username
https:www.medium.com/api/v1.0/stories/waseem_khan
// RESPONSE
{
"isSuccess": true
"message": "Data loaded successfully"
"data": 
  [
    {
      "storyId": 101,
      "clapsCount": 10,
      "title": "Highly Customizable Network layer in Swift 5",
      "published": true
    },
    {
      "storyId": 102,
      "clapsCount": 0,
      "title": "Are you an architect for an iOS app?",
      "published": false
    }
  ]
}

Response to the above API can be directly mapped to the Array of Stories i.e. [Story]

class Story: Codable {
  var storyId: Int?
  var clapsCount: Int?
  var title: String?
  var published: Bool?
}
Why is our model class Story implementing Codable?
// A type that can convert itself into and out of an external representation.
typealias Codable = Decodable & Encodable

We will map the JSON response for the API get-all-stories to Story, therefore story should implement Codable to support encoding and decoding.

Let's create our first Network API Request

Let’s create an API that fetches stories of a particular user from medium.com. This API expects records limit and page number (assuming paging is enabled) in the request body.

class GetStoriesByUsernameRequest: ApiRequest<[Story]> {
 var username: String!
 var limit = Int?
 var pageNumber = Int?
 override func apiResource() -> String {
   return "stories/"
 }
 override func endPoint() -> String {
   return "\(username)"
 }
 func bodyParams() -> NSDictionary? 
  return ["limit": limit,
          "pageNumber": pageNumber]
 }
 override func requestType() -> HTTPMethod {
   return .get
 }
}

You will see, we have overridden only configurable properties for this API. We can override apiPath(), apiVersion() etc if required. We have also mentioned that this API will return an array of stories [Story]


2. Calling an API

We have already created a network request. Now let's see how to actually call the network API. We will use the Alamofire for communicating with the API server. Let's create NetworkApiClientthat will take care of connectivity to the API Server.

class NetworkApiClient {
func callApi<T>(request: ApiRequest<T>, responseHandler: @escaping (ApiResponse) -> Void) {
 let  completeUrl = request.webserviceUrl() + request.apiPath() +     request.apiVersion() + request.apiResource() + request.endPoint()
 var urlRequest = URLRequest(url: URL(string: completeUrl)!)
 urlRequest.httpMethod = request.requestType().rawValue
 urlRequest.setValue(request.contentType(), forHTTPHeaderField:  "Content-Type")
 urlRequest.httpBody = try?JSONSerialization.data(withJSONObject:  request.parameters()!, options: [])
Alamofire.request(urlRequest).responseData { (response) in       
    switch(response.result) {
      case .success:
        self.successResponse(request: request, response: response)
      case .failure:
        self.failureResponse(response)
    }
  }
}
// here we are going to parse the data
func successResponse<T: Codable>(request: ApiRequest<T>, response: DataResponse<Data>)
// NOT SO FAST :P I will cover this in next step i.e. parsing data
 }
}
// We will encapsulate the generic response to this class
class ApiResponse {
 var success: Bool?   // whether the API call passed or failed
 var message: String? // message returned from the API
 var data: AnyObject? // actual data returned from the API
}

3. Parse Response

What do we have so far?

  1. A network request i.e get-stories-by-username
  2. A NetworkAPIClient to send the request to API server

Now let see how to parse the response returned from the API

Go back to the NetworkApiClient and provide the implementation for the function successResponse()

import SwiftyJSON
import Alamofire
class NetworkApiClient {
 ...
  // here we are going to parse the data
  func successResponse<T: Codable>(request: ApiRequest<T>, response:   DataResponse<Data>)
   // Step 1  
   let responseJson = try JSON(data: response.data!)
   // Step 2
   let dataJson = respJson["data"].object
   let data = try JSONSerialization.data(withJSONObject: json, 
options: [])
   // Step 3
   let decodedValue = try JSONDecoder().decode(T.self, from: data)
  }
}

Step 1: parsing response to a JSON object (using SwiftyJson)

Step 2: Actual data/result is inside the data key, as discussed in the general API response above

Step 3: Here is the actual deal

We have already provided the type of model class that is expected from the API. In case of request get-stories-by-username, the mentioned type is [Story] i.e. a list of Stories. decodedValue is of [Story] type.


Put everything into the test

Knowledge without practice is useless, Practice without knowledge is dangerous

What do we have so far?

  1. A network request i.e get-all-stories-by-username
  2. A network API client to

> send the request to API server

> Parse the response returned from API service

Now we will see how to use the above knowledge to call the API and use the result.

We need to create a StoryService class that will call the get-stories-by-username API. StoryService is responsible for all CRUD operations related to Story.

typealias CompletionHandler =  (Bool, AnyObject?) -> Void
class StoryService {
func fetchStoriesByUsername(username: String, limit: Int?, pageNumber: Int, completion: @escaping CompletionHandler) {
  let request = GetStoriesByUsernameRequest()
  request.username = uesrname
  request.limit = limit
  request.pageNumber = pageNumber
  NetworkClient().callApi(request: request) { (apiResponse) in
  if apiResponse.success {
       completion(true, apiResponse.data)
    } else {
       completion(false, apiResponse.message as AnyObject?)
    }
  }
}

apiResponse.data, in this case, is an array of stories i.e [Story]. We can use this list to populate the data in our UITableView in a view controller.


I hope everything is clear. Leave a comment in case of any confusion. I would be happy to help.

Suggestions are welcome.

READ this article on Medium.com

https://medium.com/@waseem.wk22_8164/highly-customizable-network-layer-in-swift-5-1e5c1e163674

要查看或添加评论,请登录

社区洞察

其他会员也浏览了