How to cache server responses in iOS apps

Apps that communicate with a server via HTTP usually have two particular requirements: don’t make the user wait for data whenever possible, and be useful when there is no internet connection. Both are the source of much reinventing the wheel.

These are very common problems, so it shouldn’t surprise us that iOS has all the APIs we need to implement response caching and offline mode. Very little code is required, even less if your server plays nice with cache headers and you’re targeting iOS 7 and above.

The shared NSURLCache

The shared NSURLCache gives us much out of the box. If our server uses Cache-Control HTTP headers a sets the maximum age for its responses, both the shared NSURLSession (iOS 7 only) and NSURLConnection will respect this and return cached responses before they expire. We don’t need to write any code to get this functionality, and in a perfect world we would stop here.

Sadly, the world of HTTP caching in iOS is far from perfect.

Offline mode

What happens if a response has expired and the app is offline?

By default, NSURLSession and NSURLConnection will not use an expired response if there isn’t internet connectivity. This is the behavior of NSURLRequestUseProtocolCachePolicy, the default value of - [NSURLRequest cachePolicy], which simply follows the HTTP protocol cache headers to its best of its knowledge.

In most cases, showing old data is better that showing no data (exceptions being weather and stock, for example). If we want our offline mode to always return the cached data, then our requests must have a different cache policy, one that uses cache data regardless of its expiration date. Both NSURLRequestReturnCacheDataDontLoad and NSURLRequestReturnCacheDataElseLoad fit this criteria. In particular, NSURLRequestReturnCacheDataElseLoad has the advantage of trying the network if no cached response is found.

Of course, we should only use these cache policies when there is no internet connectivity. If we have some reachability observer at hand, our code could look like this:

Then we give this request to NSURLSession or NSURLConnection and hope for the best.

But not if you´re targeting iOS 6. There is a bug in iOS 6 that prevents NSURLRequestReturnCacheDataDontLoad and NSURLRequestReturnCacheDataElseLoad from working properly. If you’re targetting this version and want to implement a true offline mode you’ll have to read NSURLCache directly:

Offline mode using AFNetworking

The most excellent AFNetworking 2.0 library offers HTTP requests managers in AFHTTPSessionManager (using NSURLSession) and AFHTTPRequestOperationManager (using NSURLConnection). Both can be customised by subclassing them and overriding the method that creates the data task or operation respectively.

Subclassing AFHTTPSessionManager to modify the cache policy of URL requests can be done by overriding dataTaskWithRequest:completionHandler::

Likewise, with AFHTTPRequestOperationManager:

Note that in both cases we’re using AFNetworkReachabilityManager as our reachability observer instead of implementing our own. Another advantage of using AFNetworking.

AFNetworking does not provide a workaround for the iOS 6 bug mentioned above and most likely never will. We still have to read the cache directly if our app targets iOS 6 and want to use the mentioned cache policies.

Forcing response caching

What happens if the server doesn’t set cache headers?

Good server APIs are few and scarce, and a consistent use of cache headers is not always a given. Even if we don’t have any say on the server cache policy, we can still set the expiration date of responses by adding or replacing cache headers at client level. This should only be used as a last resort, when it’s impossible to reach or convince backend developers to add cache headers.

Forcing response caching with NSURLSession

The NSURLSession delegate receives a URLSession:dataTask:willCacheResponse:completionHandler:, as part of the NSURLSessionDataDelegate protocol. This provides a great opportunity to modify the response before caching it. If our server doesn’t provide cache headers and we’re confident it should, we can add them like this:

Forcing response caching with NSURLConnection

Likewise, the NSURLConnection delegate receives a connection:willCacheResponse:, as part of the NSURLConnectionDataDelegate protocol. The code to modify the response headers right before caching the response would be something like this:

Forcing response caching using AFNetworking

When using AFNetworking the code is slightly different depending on which HTTP manager we’re using. AFHTTPSessionManager acts as the NSURLSession delegate, so we simply implement URLSession:dataTask:willCacheResponse:completionHandler: in our subclass and call super as required by the AFNetworking documentation.

In the case of AFHTTPRequestOperationManager, AFHTTPRequestOperation has a handy block setter for connection:willCacheResponse:. This allows us to put all our cache code in the HTTPRequestOperationWithRequest:success:failure override:

Additional considerations

NSURLCache caches responses, not the result of parsing them. Complex responses might require considerable parsing time, and in those cases it might be best to cache the parsed objects in a custom cache instead of relying on NSURLCache.

Additionally, you might want to validate the response before caching it. Servers are prone to sporadically return errors (e.g., “Too many connections”), and caching such a response would make sure the user gets the same error for as long as the response doesn’t expire.

Further reading

9 thoughts on “How to cache server responses in iOS apps

  1. Shaokang Zhao

    In the “Forcing response caching” part,is it MUST to check headers[@”Cache-Control”]?
    If the response has no “Cache-Control”,does URLSession:dataTask:willCacheResponse:completionHandler: will be called?

    Reply
    1. hpique Post author

      I don’t remember and the documentation isn’t clear. It says the delegate is called if “The cache-related headers in the server’s response (if present) allow caching.”, among other conditions.

      Mind trying and replying back? It’s been a while since I work with NSURLSession.

      Reply
      1. Shaokang Zhao

        It will be always called when I use NSURLRequestReloadIgnoringLocalCacheData without Cache-Control.
        I tried “Forcing response caching using AFNetworking”,but when I relaunch the App with NSURLRequestReturnCacheDataElseLoad cachePolicy(not reachable),I can’t get the data.

        Then I subclass NSURLCache and override cachedResponseForRequest: method.In the method,I call
        NSCachedURLResponse *cachedResponse = [super cachedResponseForRequest:request];
        and get cachedResponse == nil.

        I don’t know what’ wrong.

        I use AFNetworking 2.2.0, iOS 7.1

        Reply
  2. Pingback: AFNetworking 2.0 – Forced caching

  3. Marc

    Any idea how to force that a response is cached if the server does not allow caching? Especially if the App needs to work offline, this can be important. But if the server disallows caching, none of the „willCacheResponse“ methods (from other NSURLConnection or NSURLSession) is called (at least under iOS 8). So you never get the chance to modify the headers. and nothing is ever stored in the cache.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">