How to leverage HTTP cache in iOS
One of the most difficult subject in programming is caching because there is no silver bullet to the problem and each solution comes with compromises.
In this article I want to focus on how we achieved basic caching in most of our screens by leveraging HTTP cache. The goal was to provide content to the user even if there is no internet connection, in the easiest way possible. The idea is rather simple: for all GET
requests, we cache the response that we get. Then, if there is no connection, we fetch the previous response from the cache and display a warning message to the user, informing the data may be outdated.
Caching the data
The core idea is to cache all the responses we receive.
This can be done with the method urlSession(_:dataTask:willCacheResponse:completionHandler:)
of URLSessionDataDelegate
.
The documentation of this method states:
This method is called only if the NSURLProtocol handling the request decides to cache the response. As a rule, responses are cached only when all of the following are true:
- The request is for an HTTP or HTTPS URL (or your own custom networking protocol that supports caching).
- The request was successful (with a status code in the 200–299 range).
- The provided response came from the server, rather than out of the cache.
- The session configuration’s cache policy allows caching.
- The provided URLRequest object’s cache policy (if applicable) allows caching.
- The cache-related headers in the server’s response (if present) allow caching.
- The response size is small enough to reasonably fit within the cache. (For example, if you provide a disk cache, the response must be no larger than about 5% of the disk cache size.)
Caching all the requests requires that the server returns the following headers: either an Expires:
header or a Cache-Control:
header with a max-age
or s-maxage
parameter.
Using Alamofire, here is what the code looks like:
Note that we save the current date along with the response. We will use it later to display the freshness of the data.
You may wonder what the whipeAuthenticationHeaders
method is doing here. If we save the response as is, when we get back the cached response from the local cache, the potential authentication headers would be outdated and would cause the next requests to fail because the token is expired. That’s why we remove all authentication headers before caching the response. That way they won’t be used by the application. This is ad hoc for our authentication method, but just be aware that kind of problem can happen.
Fetching the cached data
The mechanism to fetch the cached response is contained in a behavior. A behavior is an object that can execute code at various times of a request life. It’s not the topic of this article, but you can learn more here. Just remember that in our case we want to modify the response when the network call fails, and return the cached data instead of an error.
In simple terms, when we hit the network and get an error due to unreachable service (meaning an error from URLSession
), we then ask the cached response for the current url request and return it.
The display
In the rest of the app, the objects responsible to fetch data, called repositories, have method signatures like this one:
They use a CachedValue
object defined as:
Either the data is fresh when the network call succeeds, either the data is read from the cache and contains the date of the cached response.
In the view controller, we display an information bar if the data is cached to inform the end user that an error occurred and that this is not the most recent data.
When the network request fails, the result looks like this. An information bar is displayed to help the user understand what is going on.
Conclusion
We have seen how to create a local cache system based only on HTTP cache. This implementation only works for GET
requests, of course, and it’s very far from an offline experience, but the users will see some content even if they have no network connection. This is already a good start and a nice improvement compared to just displaying an error.