📜 ⬆️ ⬇️

Network layer in iOS application

Virtually any mobile application interacts with servers through their API. The developer then has the task to implement the network layer of his application. The providers of an API develop its interface, often in the same way, but it also happens that the API has its own specifics. For example, the Vkontakte API with any error in accessing their methods does not display this in the status of the response code, but displays it in the response body itself as JSON by the “error” key: that is, firstly, you will not understand by the status code whether the request was successful, and secondly, you will not know what error occurred until you change the logic for processing the response. Thus, the developer has the task of implementing a sufficiently flexible layer, control over which can be carried out at different stages of work with the server.

I want to tell how you can build a fairly flexible network layer.

Please note that this is the architecture of the network layer of the application, and not the implementation of working with the network. You can use any network framework.
')
Here is what it will look like in the end:

import UIKit class ViewController: UIViewController { let service: WallPostable = BasicWallAPI() @IBOutlet weak var textField: UITextField! @IBAction func postAction() { service.postWall(with: textField.text!) } } 

So, how does the work with the API for the end user look like (I mean the programmer who uses the layer implementation):

image

Let's open the API box a bit:

image

How do I see the Send box:

image

We form a request. Usually requests have some identical headers and in order not to register them in each request, we prepare requests in the “Request Preparation” block. Next, we send a fully compiled request using a convenient framework (in my example, I use Apple’s native framework). Send a request is always the easiest, everything starts when you receive a response.

How I see the Handle box:

image

We check the received answer for success, then in case of a successful answer we will convert it into a model that is understandable for the application and give it back. If the request is not executed successfully, then we give it to the error handling block: it gets an error code, a message if there is one, reacts to all this as we tell it, forms an error that is understandable for the application and gives.

The network layer, which I implement, divides each action and encapsulates it in the appropriate type, which will implement this unique duty. These types will be interconnected by protocols.

So, what we need:


The sender of the request needs:


The response handler in turn needs:



Now about the sender of the request. In our case, they will be the service to which the request is transmitted, it is the responsibility to send it and then transfer the response to the response processor.

 protocol Service { associatedtype ResultType: Decodable associatedtype ErrorType: ErrorRepresentable typealias SuccessHandlerBlock = (ResultType) -> () typealias FailureHandlerBlock = (ErrorType) -> () var request: HTTPRequestRepresentable? { get set } var responseHandler: HTTPResponseHandler<ResultType, ErrorType>? { get set } func sendRequest() -> Self? } 

We will return ourselves for the implementation of Method Chaining.

As you can see, I just described the protocols by which the whole system will communicate with objects within the system. This enables us to control any stage we need by simply injecting our implementation of the protocol.

It often happens that for more than one request the same code arrives at an error, but it means completely different for them. When a service is one big object that processes everything itself, problems arise that lead to “crutches” and the inevitable growth of the class and the reduction of its beauty. In this case, if you need to somehow respond to an error in your own way, you simply implement the error handler protocol and inject it into the response handler: you will not change anything else, you will expand the system and not modify it, which is good An example of an open-close principle from a system perspective. All objects fulfill one of their roles: Single Responsibility Principle. Implementation of the remaining principles, I think obviously.

The idea is to implement the service to which we give the request, say what model we want to get in the end and what type of error we expect, give if we need our own handlers for the response, errors, decoding the response. Then we just ask him to send a request and wait for an answer.

Generic programming is perfect for achieving this goal. We will generalize in order to obtain a service that works successfully with any model.

So, this is how our service looks like, sending a request:

 final class BaseService<T: Decodable, E: ErrorRepresentable>: Service { typealias ResultType = T typealias ErrorType = E var responseHandler: HTTPResponseHandler<T, E>? = HTTPResponseHandler<T, E>() var request: HTTPRequestRepresentable? var successHandler: SuccessHandlerBlock? var failureHandler: FailureHandlerBlock? var noneHandler: (() -> ())? var requestPreparator: RequestPreparator? = BaseRequestPreparator() private var session: URLSession { let session = URLSession(configuration: URLSessionConfiguration.default, delegate: nil, delegateQueue: nil) return session } @discardableResult func sendRequest() -> BaseService<T, E>? { guard var request = request else { return nil } requestPreparator?.prepareRequest(&request) guard let urlRequest = request.urlRequest() else { return nil } session.dataTask(with: urlRequest) { [weak self] (data, response, error) in let response = BaseResponse(data: data, response: response, error: error) self?.responseHandler?.handleResponse(response, completion: { [weak self] (result) in switch result { case let .Value(model): self?.processSuccess(model) case let .Error(error): self?.processError(error) } }) }.resume() return self } @discardableResult func onSucces(_ success: @escaping SuccessHandlerBlock) -> BaseService<T, E> { successHandler = success return self } @discardableResult func onFailure(_ failure: @escaping FailureHandlerBlock) -> BaseService<T, E> { failureHandler = failure return self } private func processSuccess(_ model: T) { successHandler?(model) successHandler = nil } private func processError(_ error: E) { failureHandler?(error) failureHandler = nil } } 

As you can see, all he does is to prepare the request, send it using the Apple framework and send the response to the processing to the response processor.

As you understand, the parameter of the generalized class T is the type of the final model, and E is the type of error. Knowledge of these types is more needed by the response handler, take a look at it:

 class HTTPResponseHandler<T: Decodable, E: ErrorRepresentable>: ResponseHandler { typealias ResultType = T typealias ErrorType = E private var isResponseRepresentSimpleType: Bool { return T.self == Int.self || T.self == String.self || T.self == Double.self || T.self == Float.self } var errorHandler: ErrorHandler = BaseErrorHandler() var successResponseChecker: SuccessResponseChecker = BaseSuccessResponseChecker() var decodingProcessor = ModelDecodingProcessor<T>() var nestedModelGetter: NestedModelGetter? func handleResponse(_ response: ResponseRepresentable, completion: (Result<T, E>) -> ()) { if successResponseChecker.isSuccessResponse(response) { processSuccessResponse(response, completion: completion) } else { processFailureResponse(response, completion: completion) } } private func processSuccessResponse(_ response: ResponseRepresentable, completion: (Result<T, E>) -> ()) { guard var data = response.data else { return } //      ,       guard let result = try? decodingProcessor.decodeFrom(data) else { completion(Result.Error(E(ProcessingErrorType.modelProcessingError))) return } completion(.Value(result)) } private func simpleTypeUsingNestedModelGetter(from data: Data) -> T? { let getter = nestedModelGetter! guard let escapedModelJSON = try? getter.getFrom(data) else { return nil } guard let result = escapedModelJSON[getter.escapedModelKey] as? T else { return nil } return result } private func processFailureResponse(_ response: ResponseRepresentable, completion: (Result<T, E>) -> ()) { let error = E(response) completion(.Error(error)) errorHandler.handleError(error) } } 

I deliberately deleted some lines so as not to distract from the main implementation. What do these deleted lines do? I implemented the ability to extract a nested model, if the rest of the answer you do not need. This happens, for example, when you request the wall of the VC and it returns something like:

 ["response": { "items": [{"id":1, "text": "some"}, {"id":2, "text": "awesome"}], "count": 231 }] 

and you only need items, in which case you can give the NestedModelGetter response handler, which has keyPath in the form of “response.items”, and it will pull out your model for you. Also in my implementation, you can get count, that is, a primitive type, for which you must of course give an object that data converts to this type.

The whole project is available below the link, where you can see everything.

It also shows that the response handler has a validator of success, and in the case of an unsuccessful response, we give the answer to the error handler.

I will give an example of a post wall model so that it is clear how it allows itself to be assembled from the answer:

 struct WallItem: Decodable { var id: Int var text: String } 

Models simply implement the Decodable protocol, and, of course, if the keys in JSON do not match the model names, you must also add CodingKeys to the model. All of this can be found in the Apple documentation.

I will also give an example of a validator validator for the VK API:

 struct VKAPISuccessChecker: SuccessResponseChecker { let jsonSerializer = JSONSerializer() func isSuccessResponse(_ response: ResponseRepresentable) -> Bool { guard let httpResponse = response.response as? HTTPURLResponse else { return false } let isSuccesAccordingToStatusCode = Range(uncheckedBounds: (200, 300)).contains(httpResponse.statusCode) guard let data = response.data else { return false } guard let json = try? jsonSerializer.serialize(data) else { return false } return isSuccesAccordingToStatusCode && !json.keys.contains("error") } } 

The process of assembling a service for querying a VK wall is as follows:

 let service = BaseService<Wall, VKAPIError>() service.request = GETWallRequest() let responseHandler = HTTPResponseHandler<Wall, VKAPIError>() responseHandler.nestedModelGetter = ResponseModelGetter.wallResponse responseHandler.successResponseChecker = VKAPISuccessChecker() service.responseHandler = responseHandler return service 


A bit of error. All types of errors implement the protocol:
 protocol ErrorRepresentable { var message: String? { get set } var errorCode: Int? { get set } var type: ErrorType { get set } init(_ type: ErrorType) init(_ response: ResponseRepresentable) } protocol ErrorType { var rawValue: String { get } } 


ErrorType implement my enums. Let's look at the structure of the error VK:

 enum VKAPIErrorType: String, ErrorType { case invalidAccessToken case unknownError } struct VKAPIError: ErrorRepresentable { var errorCode: Int? var message: String? var type: ErrorType = VKAPIErrorType.unknownError init(_ type: ErrorType) { self.type = type } init(_ response: ResponseRepresentable) { guard let data = response.data else { return } let jsonSerializer = JSONSerializer() guard let dataJSON = try? jsonSerializer.serialize(data), let errorJSON = dataJSON["error"] as? JSON else { return } errorCode = errorJSON["error_code"] as? Int message = errorJSON["error_msg"] as? String guard let code = errorCode else { return } switch code { case 5: type = VKAPIErrorType.invalidAccessToken default: type = VKAPIErrorType.unknownError } } } 


I think no adequate developer would like to register a service build in each controller. Me too, so let's wrap up the build process in Builder:

 protocol APIBuilder { associatedtype ErrorType: ErrorRepresentable func buildAPI<T: Decodable>(_ responseType: T.Type, request: HTTPRequestRepresentable?, decodingProcessor: ModelDecodingProcessor<T>?, nestedModelGetter: NestedModelGetter? ) -> BaseService<T, ErrorType> } 

 class VKAPIBuilder: APIBuilder { typealias ErrorType = VKAPIError func buildAPI<T: Decodable>(_ responseType: T.Type, request: HTTPRequestRepresentable? = nil, decodingProcessor: ModelDecodingProcessor<T>? = nil, nestedModelGetter: NestedModelGetter? = nil) -> BaseService<T, VKAPIError> { let service = BaseService<T, VKAPIError>() service.request = request let responseHandler = HTTPResponseHandler<T, VKAPIError>() responseHandler.nestedModelGetter = nestedModelGetter responseHandler.successResponseChecker = VKAPISuccessChecker() if let decodingProcessor = decodingProcessor { responseHandler.decodingProcessor = decodingProcessor } service.responseHandler = responseHandler return service } } 


Now we wrap it all in a separate object that will implement the following protocols:

 protocol WallGettable { func getWall(completion: @escaping (Wall) -> ()) } protocol WallPostable { func postWall(with message: String) } typealias WallAPI = WallGettable & WallPostable 

But its implementation:

 class BasicWallAPI: WallAPI { private lazy var getWallService: BaseService<Wall, VKAPIError> = { return VKAPIBuilder().buildAPI(Wall.self, nestedModelGetter: ResponseModelGetter.wallResponse) }() private lazy var postService: BaseService<[String: [String: Int]], VKAPIError> = { return VKAPIBuilder().buildAPI([String: [String: Int]].self) }() func getWall(fromOwnerWith id: String, completion: @escaping (Wall) -> ()) { getWallService.request = WallRouter.GET(id, count: 20) getWallService.sendRequest()?.onSucces({ (wall) in completion(wall) }) } func postWall(with message: String) { postService.request = WallRouter.POST(message: message) postService.sendRequest() } } 


In my case, requests are structures of the type:

 struct WallRouter { struct GET: HTTPGETRequest { var path: String = "https://api.vk.com/method/wall.get" var parameters: JSON? = [:] var headerFields: [String: String]? init(_ ownerID: String, count: Int) { parameters?["owner_id"] = ownerID parameters?["count"] = count } } struct POST: HTTPPOSTRequest { var path: String = "https://api.vk.com/method/wall.post" var parameters: JSON? = [:] var headerFields: [String: String]? var bodyString: String? = nil init(message: String) { parameters?["message"] = message } } } 


Here we come to the result, which was shown at the beginning of the article. A pleasant plus is the implementation of the Separation of Concerns principle, that is, the controller will be able to call only those methods that it needs and will not know about anything else.

Usage example


Let's also implement a method for getting the number of records on the wall, that is, the values ​​of the count field.

Expand the WallGettable protocol:

 protocol WallGettable { func getWall(completion: @escaping (Wall) -> ()) func getWallItemsCount(completion: @escaping (Int) -> ()) } 


We implement a new method in our structure:
  private lazy var getWallItemsCountService: BaseService<Int, VKAPIError> = { return VKAPIBuilder().buildAPI(Int.self, decodingProcessor: IntDecodingProcessor(), nestedModelGetter: ResponseModelGetter.wallResponseCount) }() func getWallItemsCount(fromOwnerWith id: String, completion: @escaping (Int) -> ()) { getWallItemsCountService.request = WallRouter.GET(id, count: 1) getWallItemsCountService.sendRequest()?.onSucces({ (count) in completion(count) }) } 

What is ResponseModelGetter?

 enum ResponseModelGetter: String, NestedModelGetter { case wallResponse = "response" case wallResponseItems = "response.items" case wallResponseCount = "response.count" case wallResponseFirstText = "response.items.text" var keyPath: String { return self.rawValue } } 

Call the new method:

  override func viewDidLoad() { super.viewDidLoad() getWallAPI.getWallItemsCount(fromOwnerWith: "<some id>") { (count) in print("Wall items count is \(count)") } } 

Get the console output:

 Wall items count is 1650 

Of course, it is not necessary to wrap the API with all this logic, but it is convenient, beautiful, and correct (the main thing is not to dump all API methods into one class). When testing a layer on the VC API, do not forget to insert your access_token (where you will find it to register in ReqestPreparators).

Details of the implementation you can see in the project, available by reference .

If you like the implementation, do not take the stars for it.

Source: https://habr.com/ru/post/349792/


All Articles