A fresh look at the networking topic that takes advantage of Swift’s Concurrency Model
I’ve got a confession to make: Making networking layers has always been an exciting topic for me. Since the first days of iOS programming, in early 2007, each new project represented a fresh opportunity to refine or even break the entire approach I have used so far. My last attempt to write something on this topic is dated 2017, and I considered it a milestone after the switch to Swift language.
It’s been a long time since then; the language evolved like the system frameworks, and recently, with the introduction of the new Swift’s Concurrency Model, I decided to take a further step forward and update my approach to networking layers. This new version went through a radical redesign that allows you to write a request in just a single line of code:
At this time, you may be thinking: why should I make my client instead of relying on Alamofire? You’re right. A new implementation is inevitably immature and a source of issues for a certain amount of time. Despite all, you have the opportunity to create a fine-tuned integration with your software and avoid third-party dependencies. Moreover, you can take advantage of the new Apple technologies like URLSession, Codable, Async/Await & Actors.
You can find the code on GitHub; the project is called RealHTTP.
Let’s start by defining a type for representing a client. A client (formerly
HTTPClient) is a structure that comes with cookies, headers, security options, validator rules, timeout, and all other shared settings you may have in common between a group of requests. When you run a request in a client, all these properties are automatically from the client unless you customize it in a single request.
For example, when you execute an auth call and receive a JWT token, you may want to set the credentials at the client level, so any other request incorporate these data. The same happens with validators: to avoid duplicating the logic for data validation, you may want to create a new validator and let the client execute it for every request it fetches. A client is also an excellent candidate to implement retry mechanisms not available on basic
As you may imagine, a request (formerly
HTTPRequest) encapsulate a single call to an endpoint.
If you have read some other articles on this topic, you may find often a common choice is to use Swift’s Generic to handle the output of a request.
It allows you to strongly link the output object type to the request itself. While it’s a clever use of this fantastic construct, I found it makes the request a bit restrictive. From a practical perspective, you may need to use type erasure to handle this object outside its context. Also, conceptually, I prefer to keep the request stages (fetch ~> get raw data ~> object decode) separated and easily identifiable.
For these reasons, I chose to avoid generics and return a raw response (
HTTPResponse) from a request; the object will therefore include all the functions to allow easy decode (we’ll take a look at it below).
As we said, a request must allow us to easily set all the relevant attributes for a call, especially “HTTP Method,” “Path,” “Query Variables,” and “Body.” What do Swift developers love more than anything else? Type-safety.
I’ve accomplished it in two ways: using configuration objects instead of literal and protocols to provide an extensible configuration along with a set of pre-made builder functions.
This is an example of request configuration:
A typical example of type safety in action is the HTTP Method which became an enum; but also the headers which are managed using a custom
HTTPHeader object, so you can write something like the following:
It supports both type-safe keys declaration and custom literal.
The best example of the usage of protocols is the body setup of the request. While it’s ultimately a binary stream, I decided to create a struct to hold the data content and add a set of utility methods to make the most common body structures (
HTTPBody): multi-part form, JSON encoded objects, input stream, URL encoded body, etc.
The result is an:
- Extensible interface: you can create a custom body container for your own data structure and set them directly. Just make it conforms to the
HTTPSerializableBodyprotocol to allow the automatic serialization to data stream when needed.
- Easy to use APIs set: you can create all of these containers directly from the static methods offered by the
Here’s an example of a multipart form:
Making a body with a JSON encoded object is also one line of code away:
When a request is passed to a client, the associated
URLSessionTask is created automatically (in another thread) and the standard
URLSession flow is therefore executed. The underlying logic still uses the
URLSessionDelegate (and the other delegates of the family); you can find more in the
HTTPClient takes full advantage of async/await, returning the raw response from the server. Running a request is easy: just call its
fetch() function. It takes an optional client argument; if not set, the default singleton
HTTPClient instance is used (it means cookies, headers, and other configuration settings are related to this shared instance).
Therefore, the request is added to the destination client and, accordingly with the configuration, will be executed asynchronously. Both serialization and deserialization of the data stream are made in another
Task (for the sake of simplicity, another thread). This allows us to reduce the amount of work done on the
The request’s response is of type
HTTPResponse; this object encapsulates all the stuff about the operation, including the raw data, the status code, optional error (received from the server or generated by a response validator), and the metrics data valid for integration debugging purposes.
The next step is to transform the raw response into a valid object (with/without a DAO). The
decode() function allows you to pass the expected output object class. Usually, it’s an
Codable object, but it’s also essential to enable custom object decoding, so you can also use any object that conforms to the
HTTPDecodableResponse protocol. This protocol just defines a static function:
static func decode(_ response: HTTPResponse) throws -> Self?.
Implementing the custom
decode() function, you can do whatever you want to get the expected output. For example, I’m a firm fan of SwiftyJSON. It initially may seem a little more verbose than ‘Codable,’ but it also offers more flexibility over the edge cases, better failure handling, and a less opaque transformation process.
Since most of the time, you may want just to end up with the output decoded object, the
fetch() operation also presents the optional decode parameter, so you can do fetch & decode in a single pass without passing from the raw response.
fetch() function combines both the fetch and decode in a single function; you may find it helpful when you don’t need to get the inner details of the response but just the decoded object.
Using a custom client and not the shared one is to customize the logic behind the communication with your endpoint. For example, we would communicate with two different endpoints with different logic (oh man, the legacy environments…). It means both the result and errors are handled differently.
For example, the old legacy system is far away from being a REST-like system and puts errors inside the request’s body; the new one uses the shiny HTTP status code.
To handle these and more complex cases, we introduced the concept of response validators, which are very similar’s to Express’s Validators. Basically, a validator is defined by a protocol and a function that provides the request and its raw response, allowing you to decide the next step.
You can refuse the response and throw an error, accept the response or modify it, make an immediate retry or retry after executing an alternate request (this is the example for an expired JWT token that needs to be refreshed before making a further attempt with the original request).
Validators are executed in order before the response is sent to the application’s level. You can assign multiple validators to the client, and all of them can concur to the final output. This is a simplified version of the standard
You can extend/configure it with different behavior. Moreover, the
HTTPAltResponseValidator is the right validator to implement retry/after call logic. A validator can return one of the following actions defined by
nextValidator: just pass the handle to the next validator
failChain: stop the chain and return an error for that request
retry: retry the origin request with a strategy
One of the advantages of Alamofire is the infrastructure for adapting and retrying requests. Reimplementing it with callbacks is far from easy, but with async/await, it’s way easier. We want to implement two kinds of retry strategies: a simple retry with delay and a more complex one to execute an alternate call followed by the origin request.
Retry strategies are handled inside the
URLSessionDelegate which is managed by a custom internal object called
The following is an over-simplified version of the logic you can find here (along with comments):
If you are thinking about using auto-retries for connectivity issues, consider using waitsForConnectivity instead. If the request does fail with a network issue, it’s usually best to communicate an error to the user. With NWPathMonitor you can still monitor the connection to your server and retry automatically.
Debugging is important; a standard way to exchange networking calls with backend teams is cURL. It doesn’t need an introduction. There is an extension both for
HTTPResponse which generates a cURL command for the underlying
Ideally, you should call
cURLDescription on request/response and you will get all the information automatically, including the parent’s
This article would have been a lot longer. We didn’t cover topics like SSL Pinning, Large File Download/Resume, Requests Mocking, and HTTP Caching. All these features are currently implemented and working on the GitHub project, so if you are interested you can look directly at sources. By the way, I’ve reused the same approaches you have seen above.
At this time, we have created a modern lightweight networking infrastructure.
But what about our API implementation?
For smaller apps, using
HTTPClient directly without creating an API definition can be acceptable. But it’s generally a good idea to define the available APIs somewhere to reduce the clutter in your code and avoid possible errors due to duplication.
Personally, I don’t like the Moya approach, where you model APIs as an enum, and each property has a separate switch. I think it’s generally confusing because you have all the properties which configure a request scattered and mixed in a single file. Ultimately, it’s hard to read and modify and when you add a new endpoint you should move up and down through this big chunk of code.
My approach is to have an object which is able to configure a valid
HTTPRequest ready to be passed to a
HTTPClient. For this example, we’ll use the MovieDB APIs 🍿 (you should register for a free account to get a valid API Key).
Let’s now use our built network layer as a practical example. For sake of simplicity, we’ll consider two APIs: one to get upcoming/popular/top rated movies, another for search.
First of all, we want to use namespacing via enum to create a container where we’ll put all the resources for a particular context, in our case
A Resource describes a particular service offered from a remote service; it takes several input parameters and uses them to generate a valid
HTTPRequest ready to be executed. The
APIResourceConvertible protocol describes this process:
Search is a Resource to search for a movie inside the MovieDB.It can be initialized with a required parameter (
querystring) and two other optional parameters, (release)
request() function generate a valid request according to the MovieDB API doc. We can repeat this step for each to create a
Lists Resources to get the ranking list for
topRated movies. We’ll put it into the namespace
MoviesPage represent a
Codable object which reflects the result of each call of the MovieDB: With this approach, we got three benefits:
- API Calls are organized in namespaces based upon their context
- Each Resource describes a type-safe approach to create a remote request
- Each Resource contains all the logic which generate a valid HTTP Request
One last thing: we should be allowed an
HTTPClient to execute a
APIResourceConvertible call and return a type-safe object as described. This is pretty easy as you can see below:
Finally, we’ll create our
and we can execute our calls:
You can find the complete source code for this example here.
Now, we have an easy-to-use modern networking layer based upon async/await that we can customize. We have complete control over its functionality and a complete understanding of its mechanics.
The complete library for networking is released under MIT License, and it’s called RealHTTP; we are maintaining and evolving it. If you liked this article, please consider adding a star to the project or contributing to its development.