In this two part series, we will go through the networking layer we use in our iOS app at Clue.
- Part 1 covers how we moved our networking layer to a cleaner more modular architecture 🤓
- Part 2 includes a demo project on how we use it to make our network requests 🥳
Let’s get started!
Our iOS app makes use of a third-party library to handle the finer details of the networking requests to our server. Our interface to this library used to be a singleton class, through which all our service calls would be executed and handled.
Whilst this worked and was “okay” for one or two service calls, it very quickly got out of control as our app grew and we needed something better!
Let’s see why this old implementation was so limiting and what we wanted to achieve with the new architecture.
Limitations of the old implementation
- The singleton class had too many responsibilities. It needed to construct each service call with the endpoint, body and parameters as well as handle the success and failure of each call.
- It lacked separation of concerns between the networking and the business logic layer.
- It lacked testability as it required mocking of the backend and extensive test case setups.
- It was tightly coupled with a third party library, making it harder to replace if needed.
Goals of the new architecture
- Move away from the singleton class anti-pattern. Instead of having a singleton class for all the requests, each request is a class of its own and builds up its networking stack.
- Improved testability.
- Increased decoupling between the networking and the business layer by separating the response handling from the network requests implementation.
- Easy to use interface to create a network request.
Let’s now go through the main building blocks of the new architecture.
The HTTPClient class is responsible for performing the actual network request and conforms to the HTTPClientProviding protocol. We abstract this in a protocol to ensure testability.
A. The delegate is needed to handle the success or error response once the request has been performed. The delegate will typically be the Request class conforming to the HTTPClientDelegate protocol.
B. The performRequest is needed to perform the actual service call. It takes as parameter a RequestData which consists of an API endpoint, body parameters and the HTTP method for the request.
A Request class is the main touch point between the app logic and the networking layer and would typically map to one functionality eg a LoginRequest, a FetchProfileRequest etc.
- Each request encapsulates knowledge of its own RequestData i.e. API endpoint, body parameters and the HTTP method.
- Each request executes its service call through its own instance of the HTTPClient class and conforms to the HTTPClientDelegate protocol detailed above.
Most requests will be interested in the server’s response to properly handle it i.e. update a view, show a success or error message, make another follow-up request etc.
There can be several interested objects in the request’s results and whilst we could bundle all of it in a callback, it is cleaner to separate them especially when the response handling consists of unrelated concerns.
To achieve this, we make use of an observer mechanism with our RequestObserving protocol. A request that requires response handling needs to conform to this protocol.
The RequestEventListenerFactory provides
- an array of eventListeners which will list all the interested listeners that will be notified in the service call’s completion handler.
- a notifyListeners method. We have an extension with the default implementation i.e. to go through the array of listeners to notify them all of any events the request may broadcast.
Each request event listeners need to conform to the RequestEventListening protocol. Each listener then only performs one specific thing when it gets notified.
Each request will have as many request events as needed and would typically include at least a success and an error event.
For example, a LoginRequest could consist of:
- On success, an event consisting of a userId and an apiToken.
- On failure, an error.
A good example of this observer mechanism at work would be the login request. Upon successful login, multiple things need to happen
- obtaining a push token and sending it to the backend
- tracking analytics events
- fetching the user’s profile
All these tasks can now be implemented within their own event listener classes ensuring a separation of concern and testability.
We have covered how we moved our networking layer to a cleaner more modular architecture and went through the main building blocks making up this network layer.
So far, we think we have achieved our goal of improving testability, separating concerns and decoupling the networking from the business layer. And creating a new network request is nice and simple.
Check out Part 2 which includes a demo project where you can see it in action.
Thanks for reading! ❤️