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
- 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.
- The only layer that should call the networking API is the Service layer.
- The view controllers should ask the Service to provide the data.
- The Service should not know about the API server and the format of the data that is sent/received to/from the API server.
- Only Network Layer should know these details.
What this blog will cover?
- Prepare an API Request: <ApiRequest>
- Calling an API: <Alamofire>
- Parse Response: <Codable>
1. Prepare a Request
Every network request is identified by certain properties i.e.
- Endpoint: The path to the network API
- Http method type: GET, POST, PUT, DELETE, PATCH, etc
- Request parameters: JSON or a string
- Response type: JSON, XML 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. LoginApiRequest, SignupApiRequest, GetStoriesApiRequest. 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?
- A network request i.e get-stories-by-username
- 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?
- A network request i.e get-all-stories-by-username
- 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