Network Handling in Swift with Moya and Codable
Our mobile apps in Volt Lines are thin client apps that depend a lot on data received from our server’s API. Because of that, the networking layer in the apps is relatively large.
We use Moya library as an abstraction layer for networking to encapsulate network request calls and API endpoint definitions, and it has many useful features that make it easy to implement networking layers such as:
- Compile-time checking for correct API endpoint accesses.
- Lets you define a clear usage of different endpoints with associated enum values.
- Provide a Swift-first API/abstraction for making network requests.
- Provide an API that makes it easy to stub network requests.
- Provide basics of response decoding.
- Favor explicitly-defined behavior over default implementations.
API Definition:
As an example, I will explain what I implemented for sending/receiving users rating to/from our API.
So to setup Moya first we need to implement an enum with all of our API targets, we will name it RatingService
which is an enum contains definitions for rating API requests:
We defined here 2 API endpoints:
- rate(driver: Float?, bus: Float?)
a POST request that includes the driver rating and bus rating within the request body, this is why we returnedTask.requestParameters
for task property. - fetchRating
a GET request that returns a JSON contains driver and bus rating
Also, we have defined a global constant variable RatingProvider
in this file that contains an instance of MoyaProvider
which is the request provider class provided by Moya library that implements the logic of sending requests, and all the requests should be made through this class only.
Since our app (or any app) has more features, we have more APIs than rating API. Because of that, we created an enum for each feature or module in the app, and to extract some common logic from all API definitions we created a base protocol that all APIs implement, this protocol inherits TargetType
(that define the specifications necessary for a MoyaProvider
) and has an extension provide default headers property which will return the headers we need to send in all our requests like “App-Version” for example. We named this protocol BaseService
:
P.S. if you are not going to create that common protocol you will need all your API enum definitions to implement the protocol
TargetType
provided by Moya and add the read only property:
var headers: [String : String]?
Network Handling:
That’s for the definition part, what about sending requests and handling responses?
For that, we implemented a class RatingNetwork
that contains 2 methods that make the 2 requests defined in RatingAPI
:
- fetchRating(success:, failure:)
handles `fetchRate` endpoint - rate(driver:, bus:, success:, failure:)
handles `rate(driver:, bus:)` endpoint
As you expected; we also have implemented an XXNetwork
class for each feature or module for this layer in the app, and we have a base class, that all networking classes inherit, and that base class process and handle the received responses or errors in a general way. And because we have such a base class we only need to write 2 lines of code to take advantage of the common logic needed to process the response like this:
We call the method request(target:,completion:)
from the RatingProvider
instance, passing the enum that expresses the endpoint we want to request as the first parameter target
, and the complete closure that will be called after the response is received as the second parameter.
The object received in the completion closure is of the type Result<Moya.Response,MoyaError>
, enables us to check HTTP status and custom errors, and provides the response body that we parse/decode to the class we expect to receive. All this logic is handled in the BaseNetwork
class by calling self.processResponse(...)
method.
As you can notice the rate
endpoint doesn’t specify any object in the success closure since we only care about success or failure of this request, on the other hand fetchRating
endpoint an instance of the class VLRatingResponse
-contains the current driver rating info- is expected in the success closure, this class, of course, will implement the Codable
protocol to be parsed from the JSON response we received from our server, this is how it looks:
The Core of Networking:
Finally, the BaseNetwork
class which provides the common logic of handling errors and parsing responses and will be inherited by all networking classes:
In this class, we have the method processResponse
that’s called from its subclasses (as you remember form RatingNetwork
) after receiving the response from Moya library, this method has 2 overloaded versions, one that has a success closure that has no parameters and another that has a generic success closure with a parameter implements the Codable
protocol. In the first method, we only check for HTTP errors (by calling the method validate(...)
) and we don’t care about the returned response body if it was a success. On the other hand, in the second overloaded method, we parse the response body after validating HTTP success to instantiate an object of the class T
passed within the success closure.
In the method validate(...)
we are checking for HTTP success status code (2XX) to return success, or in case of HTTP errors; we check if the status code was an authentication error (401 or 403), so log out the user if he was logged in, and we extract the error message from the response body if exists to return it with the failure closure call.
Bonus
If you wondered what is the PresentableError
class, it is a simple error class with title and message, could be represented in the UI within our custom alerts, here is it:
Summary
It is pretty easy to start using Moya for networking and Codable for parsing JSON responses, but after your API grows up, your networking classes grow up with it, and you’ll find yourself repeating many lines of code, so you need to make use of inheritance and generalization to make the code as small and maintainable as you can, and that’s what we did. We extracted the protocol BaseService
to be inherited by all our API definition enums and implemented the common properties in an extension of this protocol. And for the networking part, we extracted the class BaseNetwork
that handles errors, validates successful responses, and parses response bodies using generic methods.
Btw, we are hiring!
Please visit our careers page for more details.