Network core as part of the application
To begin with, I will explain a little about what will be discussed in this article. Now the majority of mobile applications, in my opinion, are client-server. This means that they contain, as part of the code, a network core responsible for sending requests and receiving responses from the server. Moreover, this is not about network libraries that take responsibility for "low-level" request management, such as sending REST requests, building a multipart body, working with sockets, web sockets, and so on. This is an additional binding that allows you to manage requests, responses and status data specific to your server. It is in the embodiments of this binding that the main problems of the network layer are contained in many mobile projects with which I had to work.
This article aims to bring
one of the architectural solutions for building the network core of the application, which I came to after a long time working with different models and different server APIs, and which is currently the most optimal for the tasks I encounter in the process of working on projects . I hope this option will help you develop a well-structured and easily expandable network core if you start a new project or modify an existing one. I will also be glad to hear your advice and comments on improving the code and / or the proposed architecture. And yes, the article due to the large volume will be released in two parts.
Details under the cut.
Client-server interaction
In the course of work, you have to disassemble a lot of various projects that in one way or another interact with servers built using the REST protocol. And in most of these projects, there is a picture from the category “who is in the forest, who is for firewood”, since the network core is everywhere implemented differently (however, like other architectural parts of the application). The situation is especially bad when in one project one can see the hand of several developers who have succeeded each other (and, moreover, under tight deadlines, as a rule). In such cases, it often turns out that the “core” of a rather creepy look, almost having its own intellect. Let's try to protect ourselves from such problems with the help of the proposed approach.
')
Where do we start?
- To implement we will use the language Swift version 3.0.
- Let's agree that we will use a hypothetical REST server that works only with GET and POST requests. As answers, he will return us either JSON or some data (unfortunately, not all servers have a unified form of response, so we have to provide different options).
- We also agree that for a “low-level” network communication we do not need a separate network library, we will write this part ourselves (especially at the time of this writing, the network libraries updated to version 3.0 of the language simply do not exist).
- The proposed approach is a stripped-down version of the SOA (Service-Oriented Architecture) architecture, with remote parts not used in small projects (such as a separate caching subsystem) adapted to the strict typing conditions of the Swift language.
- Xcode Playground will be enough for us to implement, a separate project is not required.
Let's get started First, a little about the process itself. We describe the step-by-step scheme of the client mobile application with the server:
- The user performs an action that requires interaction with the server.
- The application sends a GET or POST request to the server through the network core, specifying a specific URL, header set, request body, timeout, cache policy, and other parameters.
- The server processes the request and sends (or does not send, if something went wrong with the communication channel) response to the application.
- The application analyzes the information received, updates the state data and the user interface.
And what's the difficulty?
And really, as long as everything sounds pretty simple. However, in reality, these 4 simple points contain additional steps that require additional labor, and this is where the diversity begins in terms of the implementation of these very intermediate steps. Let's expand our sequence a little, having added it with the questions arising in the course of work:
- The user performs an action that requires interaction with the server.
- Does user action depend on the status data associated with the server? For example, does the action require an authorization token? If so, where to store it, how to check it?
- The server API can consist of several “access points”, for example, an authorization node, a user data processing node, and so on. It would be nice to somehow divide these actions into groups, so as not to dump all the code into one big sheet.
- The user may want to cancel his action (if the application gives him the opportunity). We should also be able to handle such situations.
- The application sends a GET or POST request to the server via the network core.
- We have a separate base URL of our server (there may be several of them), relative to which the absolute address is built. It needs to be somehow set in order not to spend a lot of code not this same type of action, but at the same time make our solution customizable.
- Each request may have a mandatory set of headers, as well as additional headers required for a particular request, including the types of data sent and received.
- The request can be launched in a special session (speech about URLSession), with a specific timeout and data caching policy.
- It would be nice to have a list of all asynchronous requests being executed at the moment; this can be useful both from the point of view of analytics and from the point of view of debug information.
- The server processes the request and sends (or does not send, if something went wrong with the communication channel) response to the application.
- Since the receipt of the response is an unstable event, we definitely need to be able to handle not only the answers, but also the generated errors. And given that server APIs often do not have any kind of unified structure of responses and error output, you should be prepared to receive a variety of error formats, including system ones.
- Since, in a successful response (depending on the implementation of the server API), we may receive both a successful response itself and a server’s failure to process the operation (for example, if we forgot to specify an authorization token for a request closed by user authorization), we need to be able to process successful responses as well as “successful mistakes”. At the same time, the handlers themselves should preferably be placed in the form of self-contained separate entities in the code, which can be easily analyzed and accompanied.
- The application analyzes the information received, updates the state data and the user interface.
- This item again brings us back to the issue of storing state data. We must select the data from the server response that we need to display the user interface, as well as the data that may be needed for further requests to the server, process them and save them in a convenient form.
It would seem that a fairly simple set of questions, but due to the number and variety of tools available in the language, the implementation of all these stages from project to project may vary beyond recognition. By the way, with a small modification, the proposed approach can also be applied in Objective-C (as a language with weak typing), but this is beyond the scope of this article.
Design
To solve all the above questions, we need several entities:
- Information about the "successful error." This entity will provide receiving and storing information about the "successful error" or "server failure", as I most often call it - for example, an authorization error.
- Server response information. It will store all the information received from the server so that it is guaranteed that there is no shortage. Sooner or later, projects have to analyze what has come from the server, down to the URLResponse parameters, so it’s best to keep all such data in quick access.
- Asynchronous network task. Actually the asynchronous task itself, which stores its state, as well as input and output parameters, including, of course, the server request itself.
- Task pool The class is engaged in sending and accounting for active asynchronous network tasks. It also stores general data on the server API, such as the base URL, standard header set, and so on.
- Response processing protocol This protocol will provide us with the ability to create strongly typed entities ( “parsers”, or handlers ) that can take data stream (including formatted JSON) as input and transform it into application-friendly structures or data models (about them below).
- Data models Data structures that are understandable and user-friendly. They are a reflection of server data in a form adapted for the application.
- Parsers, or response handlers. Entities that transform raw data into data models, and vice versa. Implement the response processing protocol.
- Service, or data access node. Encapsulates the management of a group of logically related operations. For example, AuthService, UserService, ProfileService, and so on.
If it seems to you that there are too many links here, initially I thought so too. Until I tried several projects with a lack of a designed network core in principle. Without a clear structure, after the implementation of 5-10 requests to the server, the code begins to quickly turn into a mess, if left unstructured. Again, I propose only one of the approaches, and at the moment it is he who is most convenient for me in working on projects.
For clarity, I reflected the whole process in the diagram (the image is clickable):

Kernel implementation
Go. Create a Playground and go straight along the steps from the previous section. Plus, suppose that in most cases my server returns me JSON. Which JSON parser to use is at your discretion, at the time of writing this article, again, there were no libraries adapted for the third version of the language, so I used the self-written GJSON parser.
Error info
It's all pretty basic, a class with a pair of fields and an initializer. We finalize it, as part of our task, we don’t need extensions (of course, this may be different in your project):
Error infofinal class ResponseError { let code: NSNumber? let message: String? // MARK: - Root init(json: Any?) { if let obj = GJSON(json) { code = obj.number("code") message = obj.string("message") } else { code = nil message = nil } } }
Response information
Slightly more detailed class, the principle is the same. In this case, it also corresponds to the response returned by the server, this is binary input data, response code, optional message + parsed error:
Response information final class Response { let data: Any? let code: NSNumber? let message: String? let error: ResponseError?
For convenience in your project, if you work with guaranteed JSON in the returned binary data, you can extend this class through extension, for example, by adding a field that returns a json parser (as an example) to it:
Expansion for convenience extension Response { var parser: GJSON? { return GJSON(data) } }
Asynchronous network task
This is a much more interesting thing. The purpose of this class is to store all the data returned, all the received data and its state (completed / canceled). It can also send itself to the network and call a callback after completion of the work. Upon receiving a response from the server, it records all received data, including attempts to transform the input data into a JSON object, if possible, to simplify subsequent processing. Also, since we will use the set of such tasks in the future within the Set, we will need to implement a couple of system protocols.
Asynchronous network task typealias DataTaskCallback = (DataTask) -> () final class DataTask: Hashable, Equatable { let taskId: String let request: URLRequest let session: URLSession private(set) var responseObject: Response? private(set) var response: URLResponse? private(set) var error: Error? private(set) var isCancelled = false private(set) var isCompleted = false private var task: URLSessionDataTask? // MARK: - Root init(taskId: String, request: URLRequest, session: URLSession = URLSession.shared) { self.taskId = taskId self.request = request self.session = session } // MARK: - Controls func start(callback: DataTaskCallback?) { task = session.dataTask(with: request) { [unowned self] (data, response, error) in if self.isCancelled { return } self.isCompleted = true // transform if possible var wrappedData: Any? = data if data != nil { if let json = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) { wrappedData = json } } // parse self.responseObject = Response(json: wrappedData) self.error = error // callback callback?(self) } task?.resume() } func cancel(callback: DataTaskCallback?) { if task != nil && !isCompleted && !isCancelled { isCancelled = true task?.cancel() // callback callback?(self) } } // MARK: - Equatable static func ==(lhs: DataTask, rhs: DataTask) -> Bool { return lhs.taskId == rhs.taskId } // MARK: - Hashable var hashValue: Int { return taskId.hashValue } }
Task pool
As mentioned earlier, this class will keep track of active asynchronous tasks, which can be useful when analyzing or debugging. As a bonus, it allows you to find a task by its identifier and, for example, cancel it. Through the task pool, all tasks are sent to the network (it can be done without it, but then you have to store the task somewhere independently, which is not very convenient). Class doing singlton, of course.
Task pool final class TaskPool { static let instance = TaskPool() // singleton let session = URLSession.shared // default session let baseURLString = "https://myserver.com/api/v1/" // default base URL var defaultHeaders: [String: String] { // default headers list return ["Content-Type": "application/json"] } private(set) var activeTasks = Set<DataTask>() // MARK: - Root private init() { } // forbid multi-instantiation // MARK: - Controls func send(task: DataTask, callback: DataTaskCallback?) { // check for existing task if taskById(task.taskId) != nil { print("task with id \"\(task.taskId)\" is already active.") return } // start activeTasks.insert(task) print("start task \"\(task.taskId)\". URL: \(task.request.url!.absoluteString)") task.start { [unowned self] (task) in self.activeTasks.remove(task) print("task finished \"\(task.taskId)\". URL: \(task.request.url!.absoluteString)") callback?(task) } } func send(taskId: String, request: URLRequest, callback: DataTaskCallback?) { let task = DataTask(taskId: taskId, request: request, session: session) send(task: task, callback: callback) } func taskById(_ taskId: String) -> DataTask? { return activeTasks.first(where: { (task) -> Bool in return task.taskId == taskId }) } }
Protocol
And the final part, relating directly to the network core, is the processing of responses. Strong language typing makes its own adjustments. We want our parser classes to process the data and give us a specific data type, not some Any ?. For these purposes, we will make a small generic protocol, which we will further implement:
Protocol protocol JSONParser { associatedtype ModelType func parse(json: Any?) -> ModelType? }
What's next?
Actually, on this creation of a network kernel is complete. It can be easily transferred between projects, only slightly podhertovyvaya structure information about the response and error, as well as the address of the server API.
In the second part of this article, we will look at how to use the created network core in a project in order to maximize the benefits of the proposed architecture.