Not so long ago I had published an article about Network Layers and how Swift may help us avoiding big fat singletons by isolating responsibility and simplifying the codebase (it’s on Medium).
Just wanted to say how much I appreciate the time many people took to read it sending lots of comments via mail and twitter.

During the past five months I had the chance to test it on different production projects, discuss it with co-workers and colleagues: the following article aims to propose a more robust and stable iteration of the initial idea: some stuff are changed while others still here, stronger than ever.
In order to keep it readable from anyone I’ll describe it from the scratch and I’ll provide a real implementation you can download and use in your next application.

Table of Contents

The common networking layer

Recently there are lot of shiny iOS architectures that are getting more and more hype. As they are all valid and have good and bad parts, they all address the same thing: separate the business logic from the presentationIn this article I’ll describe how to avoid a classic pitfall during iOS development: handle with networking.

But, to explain my point of view, I will first talk a bit about how network layers are often implemented.
In most of them there something called NetworkManager: it’s just a single class (more often a singleton) that contains every single API call in the app.

While it works fine (and apparently make sense) it fails miserably for several reasons:

  • Single responsibility: Our NetworkManager contains too many responsibilities to be considered a good practice (if you are interested Marco wrote a good article about it)
  • Singleton: often demonized, singletons are not necessarily bad, but they can’t be injected as dependencies.
  • Fail for testing: your code can’t be easily mocked when testing; reading raw data or checking why the code crush maybe a pain.

A different approach: what you will get

Before showing how the architecture was designed I’ll show to you what you will get using this approach.
Your next call maybe as below:

It’s cool:

  • Automatic asynchronous support
  • Promise based implementation
  • Out-of-box implementation from raw response -> your model
  • Strong error handler

Architecture design

While the theory behind this approach is independent from tools used, in order to give a complete out-of-box approach I’ve used the following libraries:

  • Networking: in this example the Service  implementation uses Alamofire. Switching to NSURLSession  is pretty damn easy and, in fact, suggested.
  • Async/Promise: I love promises (at least until we’ll get something better with Swift 5) because they are clean, simple and offer a strong error handling mechanism.
    Our networking layer uses Hydra (a library of mine), that recently hits the 1.0 milestone.
  • JSON: The higher level of the architecture offer out-of-box JSON support with JSONOperation class: everything about JSON was offered by SwiftyJSON, probability the best library for these stuff.
    If you like magic you can still plug something like ObjectMapper (frankly I love to control my code and a bunch of code lines does not change my life, especially when you need to debug something).

The following graph shows the architecture of our networking layer:

While it may seems complex, in fact its pretty simple to understand:

  • ServiceConfig is responsible to keep the network configuration data (full host address, headers…). You can create it programmatically or (strongly suggested) configuration can be read directly from your app’s Info.plist file. We’ll cover it later below with a cool trick you may not know.
  • Service: it’s a concrete implementation of the ServiceProtocol . It’s the core of the networking layer, created via  ServiceConfig. This class is responsible to fulfil your requests; in fact its the isolation layer between a request and how it will be dispatched: concrete implementation currently relay to Alamofire but you are free to plug-and-play your own engine (new versions of NSURLSession  are pretty good).
  • Request & Response: both are concrete representations of RequestProtocol  and ResponseProtocol ; as you may imagine they identify a single network request (with all required parameters and facilities) and a response from server.
  • JSONOperation & DataOperation: they are the higher level of the abstraction. You will subclass them to provide an out-of-box implementation of the “Raw Request to Model” paradigm.

Prepare schema-based environments

As I said ServiceConfig  is responsible to create the configuration of our network environment. At this time it basically encapsulates informations about the hostname, environment name and optional headers to use in each call.

You can create it programmatically

Or you can leave the class obtain it automatically:

The last choice is what I suggest because is more robust and allows you to configure different environment using XCode’s schemas.
First of all you need to add your own configuration in Project’s Info under Configurations section; below I’ve created two environment (Testing and Production server) both for Debug and Release.

 

The second step is to create different User Defined Settings, one for each configuration.

Select your App’s Main Target and go to Build Settings, then tap the “+” button on the top bar to select “Add User-Defined Setting“. We will add at least two settings, one for server’s URL and another for environment name.

Next we need to provide values for our networking later: go to your App’s Info.plist and add a new Dictionary entry named endpoint ; then set url  and name  to our variables (all lowercased).

Finally you need to create two schemas, one for Testing and another for Production inside Schemes panel.
Below you can see the Production environment where Run configuration uses our Debug-Production environment (similarly you may want to set the Archive to Release-Production).

In this way you can Run/Archive your app using your Test/Production environment easily.

The Service Component

Service  is a concrete class which belongs ServiceProtocol ; its primary role is to abstract how a Request  is fulfilled. In this implementation we have used Alamofire as networking library but as isolation layer you are free to replace it with your own ( NSURLSession  is really great with the last updates).
Service is allocated with a ServiceConfig  and keep the current environment’s headers list; for example your Login operation may set an Authorization header automatically once its logged in and share it with all other requests which belong its Service .

Service require the implementation of only one fun called execute: it must return a Promise with the response (as ResponseProtocol). The implementation below shows how it works with Alamofire:

The main point here is the following line:

Request class always return an URLRequest  instance which encapsulate all the Request’s parameters (body, fields, enpoint and so on).
In this case we need to create an Alamofire object called DataRequest  before using it with the library; using URLSession  as inner engine you can still use it to perform a request.

Received response is of type Response ; we will see it in depth later.

Prepare a Request

As we seen Request  is a concrete subclass of RequestProtocol ; their role is to abstract how the Request is created. Each request allows you to set the following parameters:

  • Endpoint: the url of the API call (ie. "/auth/login" )
  • Method: the HTTP method of the request (POST, GET…)
  • Fields (Optional): you can pass a Dictionary ([String:Any?])  of parameters: Request  will compose it (ignoring automatically null values) to create a form encoded url (ie. passing ["p1":"a","p2":55,"p3":nil]  fields portion of the url will be "?p1=a&p2=55 – all parameters will be converted automatically to support url encoding).
  • URL Parameters (Optional): this is another Dictionary ([String:Any?])  used to compose a dynamic endpoint. If your endpoint require dynamic values (ie. "/articles/{category}/id/{art_id}" ) you can use this dictionary to fill up placeholders (ie. passing ["category":"scifi","art_id":5999] ).
  • Body (Optional): this the body of the request; its a struct of type RequestBody  where you can specify the data to put in body. You can create a JSON Body using let body = RequestBody(json: data)  (you can pass any JSON serializable structure); URL Encoded Body is supported (let body = RequestBody(urlEncoded: paramsDict) . Finally you can create a body with raw String  or plain Data  or provide your own encoding.
  • Headers (Optional): you can set a [String:String] Dictionary  of values you want to attach to the Request instance. The list of Headers is composed as merge between Service ‘s current session headers and the Request  headers (in case of duplicate Request ‘s headers wins).

This is an example of Request you can create:

Everything is completely isolated by the innet implementation.

Parse the Response

Response is the concrete implementation of ResponseProtocol protocol; its role is to abstract the response from server. The Response implement the following features:

  • Type ( type ): its of type Response.Result  and maybe success  (if you have received a 2xx response from server), failure  (for 4xx) or noResponse  (if request cannot be dispatched to the end server)
  • Request ( request ): its a reference to the Request instance that raised this Response. You can use it for unit test to verify all params sent to server.
  • HTTP Response ( httpResponse ): contains the HTTP status code of the response
  • Data ( data ): contains raw data received from server
  • Metrics ( metrics ): contains all the benchmark data of the request (start time, end time, response time, latency and duration)

Plus it implements two important functions:

  • toJSON()  which parse the JSON and return a SwiftyJSON JSON structure. Parsing is done automatically and cached.
  • toString() which return a readable string of the Data  received from server.

As you seen above Response  is created automatically from the execute function of our Service  implementation. The current implementation provide an init for Response  which takes as input the DataResponse  of Alamofire; however you can plug your own implementation to read other kind of results.

Create an Operation

The last part of this architecture is composed by the Operation  entity: DataOperation  and JSONOperation  are concrete implementations of the OperationProtocol  protocol.
You can think about Operation  as the higher level of abstraction for our networking layer: it encapsulate the business logic of your API call and keep it in a self contained structure.
One important role of the Operation  is to provide an out-of-box conversion of the raw/parsed (json) response from server to our application’s business models.

You can create an additional Operation subclass to provide your own implementation based upon received data type (ie. XMLOperation ).

A Login  operation may take as input parameters username  and password  and return a LoggedUserModel .
A SearchArticles  operation may take as input parameters several filters ( search term, date, category  etc.) and return an array of [Articles] .

While DataOperation simply forward received Response from the Request, JSONOperation  is far more interesting; in fact you will use it to create a model from your JSON.
Let me show to you a real example: we want to create a search operation to perform a search on Articles database of our server.

First of all we want to create our ArticleModel :

The model provide two functions:

  • the init function takes as argument a JSON and transform it in an ArticleModel
  • the static func load(list:)  takes an array of JSON and map it to ArticleModel

With our model ready we can create easily a subclass of JSONOperation  which takes search params as input and return an array of models.

As you can see SearchArticles  encapsulate the logic behind the search operation by returning as output a set of ArticleModel  objects.

In order to parse our JSON response to a valid model we need to provide a parser function to Operation ‘s onParseResponse  variable: in order to keep the logic of parsing isolated from the network stuff we simply recall the parsing functions already seen above.

With our SearchArticle ‘s  operation  in place we can execute it in our Service  istance:

Et voilà! We have made an async network request, isolating all the logic behind, in just a line of code: and it’s with Promises too!

How to use it in your App (HermesNetwork)

As usual the complete implementation of the networking layer described above is also available on GitHub. While it’s not a real library yet I’ve named it HermesNetwork (the emissary and messenger of the gods in Greek religion and mythology).
It currently uses SwiftyJSON, Hydra and Alamofire but, as I said, you can plug your own implementation of each component and the architecture behind still the same (and in fact its the purpose of this article).

If you find it interesting or you want to discuss with me about this architecture I’ll be more than happy: just tweet to me or drop a mail.

Reply to this tweet to post your comments about this article: