Introduction
Connecting the mobile apps with a REST service is almost inevitable in every project – apps would be useless and boring if they don’t get the latest data from the servers. There are a lot of great frameworks that do this for iOS, like AFNetworking and its Swift counterpart AlamoFire, Moya, APIKit and many more. They have their design differences, however at the core they are all wrappers of Apple’s NSURLSession or NSURLConnection.
AFNetworking still seems to be the most popular one, with over 26000 stars on GitHub. One of the reasons for this is that it’s the most complete framework, providing all kinds of network requests, uploads, serializers and so on. However, one thing I don’t like about AFNetworking is that chaining sequential requests is all but elegant, although there are some PromiseKit and RXSwift extensions that work through this issue. Also, if you go through the code of the framework, you will notice bits of complication and over engineering. Having all this in mind, I try to avoid using it when possible.
Moya is a wrapper of AlamoFire, and it looks pretty good. The idea of creating enumeration that implements Moya’s TargetType for all of the endpoints of the REST service is really cool and I will get back to this in more details later.
Design considerations
When you think of what a networking library should do, the job is pretty simple:
1. create a request
2. send the request
3. handle the response
The first part, the creation of the request seems to be the most complicated part. There are a lot of different requests – with different request headers, with SSL credentials, with basic authorisation, different HTTP methods (GET, POST and 6 more), different parameters, body and so on. A good networking library should provide easy and robust way of creating these requests.
The second part is probably the simplest part – making a wrapper to the system libraries that do the actual job of sending the request through the network and receiving the response. Here is the place where you will use the NSURLSession or NSURLConnection.
After the request finishes, its response should be properly handled – here you check whether the request is successfull (what’s the status code is), if it’s successfull then based on the Content-Type properly handle it – maybe parse a json/xml response and based on it maybe provide some already created model object/struct back to the caller. Here’s the place where you can attach the appropriate response serializers.
The brilliant talk from Apple’s 2015 WWDC Protocol-Oriented Programming in Swift gives one great hint when you start designing an API – start with a protocol. The above discussion leads us to the following:
This is the core thing that a networking library should have – execute a request and return the result. Later on, it can be expanded with convenience methods like get, post, put methods, but all of them at the end will end up creating a request and executing it with the above method. When you think of it this way, designing a networking API doesn’t look that complicated after all.
Now that we have the basic design, we should start thinking of possible implementation. How this Request type should look like? Should it also be a protocol, or a structure, a class? What properties should it have? The same questions arise for the Result type. And what about the execute request method, what it should exactly do?
Creating the request
The Request type smells more like a structure – it should have all of the standard things a request has:
Note that everything here is immutable – when the request is created and all of the properties are filled with data, those values should not change anymore. We are working in a multi threaded environment, so introducing mutability will also bring more complexity and bugs.
However, in the creation process we can have a helper mutable version of the request, which we will pass through the pipe of creation methods. We can define protocol for the creation of the requests:
We can define as many different methods as we need and then combine those in a chain which at the end will return the immutable request. For example, let’s say we want to create a standard request with json support:
Here, the |> is the forward pipe operator:
All of the implementations can go in protocol extensions and can be overriden if needed in a structure or child protocol implementation. The good thing is that this methods can be combined in many different creation request methods without having to duplicate code.
Also, you can take this step further and abstract away the creation of the REST service endpoints’ urls in a more type safe manner. As mentioned before, Moya does this really good. Here’s one simplified version of their TargetType:
Note that here we’ve also added the request generation protocol. What’s the difference between these two protocols? The RequestGenerator protocol is in charge of defining the type of requests we will need. Probably we will need different request generators for the different services we connect to from within the app. Let’s say our app uses some payment service which requires more secure connection – like using SSL certificates or basic authentication. We can create one request generator for the connection to this secure server and as many ServiceEndpoints as there are REST service calls which our app uses. If the app also uses some weather xml service, we will create a different request generator with the corresponding end points. In any case, we can always use the same building blocks, just combined in different ways.
An example implementation might look like this:
After all of this is setup, the users of the networking code will only need to specify which endpoint they want to call:
init (endpoint: ServiceEndpoint) – which will return the immutable request.
The benefits of this kind of architecture are that everything is decoupled – starting from the request creation blocks, all the way to the request generators (you can reuse the same request generators in different REST endpoints). Also, it’s very easy if you want to extend it and implement new request creation methods. The type safe mapping provides good overview to what kind of REST services you are using in the app, while also making its calling much simpler.
Executing the request
After the request is created, we need to pass it to an http client that will execute it. Here, there are also many options for implementation – but most likely you will implement a wrapper around NSURLSession, which works with NSURLRequests. For this reason, the first step that we need to do is to adapt our Request struct to the NSURLRequest class:
Then, we should provide an implementation to the protocol method defined earlier:
func executeRequest(request: Request) -> Result<Response, NSError>
When you look at this method, it may sound I bit unusual. We are not passing a callback which will be called after the request finishes. We are also not returning some async object like a Future/Promise. So how are we going to get the result, having in mind that the request will be few seconds (at least)? The answer is – this will be synchronous method.
This might sound a bit strange at first – the thread will be blocked, the user will wait for the request to finish without being able to do anything, the user experience will be terrible and so on. However, we are not saying here that the method will be executed on the main thread, but that it will be blocked on the same thread.
Why would we want to do this? Well, designing a synchronous API provides many benefits. First, it’s much easier to chain sequential requests. Second, when you have this initial design at the core, you can extend it with Reactive Extensions, Promises, Futures and anything else you would need. The sending of the request will not be changed, we can just provide async execution in the way we prefer.
You probably already know how to implement synchronous request in NSURLSession, so we will not go into details about that.
We are more interested into how to extend our implementation of the HTTP protocol, let’s call it HTTPClient:
You can see how easy it is to provide the needed background execution. Now we have rx_request method that returns a Driver on the main thread, which we can use if we have decided to follow more reactive design of our app. Let’s say we want to load some categories of videos from the network:
More details about RXSwift in this post.
If you are more into promises, you can do the same kind of extensions with one of the promise libraries, like PromiseKit. There, you will have a method like:
func pk_request(request: Request) -> Promise
The returned promise can then be chained with other promises and all of the benefits of the promises can be applied to the requests. And the great part of it is that we are not making any changes to our core networking library.
Handling the response
As you may have noticed already, our definition of the HTTP protocol returns Result<Response, NSError>. The result is enumeration which has Response value when the request finishes successfully and NSError when it fails. We can always wrap the NSError object to our custom Error object, but for simplicity let’s use NSError now:
Let’s use simple Response structure with body and status code just to ilustrate the point:
Note that here we are keeping only the body of the response and we are not doing any serialisation (JSON/XML) in the HTTP protocol implementation. The Result enumeration allows any object to be put as a success value, which means you can always easily extend this protocol to support Dictionary, custom object, JSON object or whatever you need in your code that uses this networking API.
Finishing thoughts
The idea with this architecture is to have a small, easily extendible core networking API that will finish the job for the most networking requirements in iOS apps. If you need something more complex, you can follow the same principle with providing extensions for what you need. The most important thing is to have nicely decoupled components, which can be reused and easily extended, while also following the basic philosophy behind a network request.
2 Comments