📜 ⬆️ ⬇️

The architecture of the network core in the iOS application on Swift 3. Part 1

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?



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:


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:


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:


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 info
final 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? // MARK: - Root init?(json: Any?) { guard let obj = GJSON(json) else { return nil } code = obj.number("code") message = obj.string("message") data = json error = ResponseError(json: json) } } 

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.

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


All Articles