Episode #15

HTTP Caching

9 minutes
Published on May 10, 2012
HTTP Caching is an important concept to understand when building iPhone apps that consume HTTP APIs. In this episode, we'll see how leveraging Etags, Last Modified dates, and Cache-Control headers can help make your app more efficient and tolerable to use.

Episode Links

Inspecting the API with curl

Curl makes it easy to test out APIs and see what type of raw responses you'll receive.

Issue a normal request to the API:

curl -i http://cache-tester.herokuapp.com/contacts.json

You'll get something like this as a response:

HTTP/1.1 200 OK 
Cache-Control: max-age=10, public
Content-Type: application/json; charset=utf-8
Date: Thu, 10 May 2012 12:51:49 GMT
Etag: "ba598025146eff37e26c9150b180a78b"
Last-Modified: Mon, 07 May 2012 01:55:57 GMT
Server: WEBrick/1.3.1 (Ruby/1.9.2/2011-07-09)
X-Request-Id: c6fa9ef1cd630a8bb1640d8a4480dca6
X-Runtime: 0.011148
X-Ua-Compatible: IE=Edge,chrome=1
Content-Length: 3413
Connection: keep-alive

[{"created_at":"2012-05-07T01:36:19Z","email":"honestabe@gmail.com","id":1,"name":"Abe Lincoln","updated_at":"2012-05-07T01:36:19Z"},{"created_at":"2012-05-07T01:36:38Z","email":"benf@gmail.com","id":2,"name":"Ben Franklin","updated_at":"2012-05-07T01:36:38Z"},{"created_at":"2012-05-07T01:37:39Z","email":"charlie_d@hotmail.com","id":3,"name":"Charles Darwin","updated_at":"2012-05-07T01:37:39Z"},{"created_at":"2012-05-07T01:37:58Z","email":"dougie_adams@aol.com","id":4,"name":"Douglas Adams","updated_at":"2012-05-07T01:37:58Z"},{"created_at":"2012-05-07T01:38:08Z","email":"enor@gmail.com","id":5,"name":"Edward Norton","updated_at":"2012-05-07T01:38:08Z"},{"created_at":"2012-05-07T01:38:23Z","email":"bourbonrocks@gmail.com","id":6,"name":"Frank Sinatra","updated_at":"2012-05-07T01:38:23Z"},{"created_at":"2012-05-07T01:38:39Z","email":"jarjar@aol.com","id":7,"name":"George Lucas","updated_at":"2012-05-07T01:38:39Z"},{"created_at":"2012-05-07T01:38:49Z","email":"modelt@gmail.com","id":8,"name":"Henry Ford","updated_at":"2012-05-07T01:38:49Z"},{"created_at":"2012-05-07T01:39:13Z","email":"revenge_fencer@gmail.com","id":9,"name":"Inigo Montoya","updated_at":"2012-05-07T01:39:13Z"},{"created_at":"2012-05-07T01:39:27Z","email":"jspringer@hotmail.com","id":10,"name":"Jerry Springer","updated_at":"2012-05-07T01:39:27Z"},{"created_at":"2012-05-07T01:39:48Z","email":"kbacon@gmail.com","id":11,"name":"Kevin Bacon","updated_at":"2012-05-07T01:39:48Z"},{"created_at":"2012-05-07T01:39:57Z","email":"wow@gmail.com","id":12,"name":"Leroy Jenkins","updated_at":"2012-05-07T01:39:57Z"},{"created_at":"2012-05-07T01:40:19Z","email":"airjordan@gmail.com","id":13,"name":"Michael Jordan","updated_at":"2012-05-07T01:40:19Z"},{"created_at":"2012-05-07T01:41:40Z","email":"nolte@gmail.com","id":14,"name":"Nick Nolte","updated_at":"2012-05-07T01:41:40Z"},{"created_at":"2012-05-07T01:41:51Z","email":"oprah@me.com","id":15,"name":"Oprah Winfrey","updated_at":"2012-05-07T01:41:51Z"},{"created_at":"2012-05-07T01:42:09Z","email":"paulaabdul@gmail.com","id":16,"name":"Paula Abdul","updated_at":"2012-05-07T01:42:09Z"},{"created_at":"2012-05-07T01:45:36Z","email":"quincy@gmail.com","id":17,"name":"Quincy Jones","updated_at":"2012-05-07T01:45:36Z"},{"created_at":"2012-05-07T01:45:48Z","email":"thered@aol.com","id":18,"name":"Robert Redford","updated_at":"2012-05-07T01:45:48Z"},{"created_at":"2012-05-07T01:46:28Z","email":"karate@gmail.com","id":19,"name":"Steven Seagal","updated_at":"2012-05-07T01:46:42Z"},{"created_at":"2012-05-07T01:48:50Z","email":"ulysses@aol.com","id":21,"name":"Ulysses S Grant","updated_at":"2012-05-07T01:48:50Z"},{"created_at":"2012-05-07T01:49:15Z","email":"vighug@hotmail.com","id":22,"name":"Victor Hugo","updated_at":"2012-05-07T01:49:15Z"},{"created_at":"2012-05-07T01:49:28Z","email":"pennypincher@aol.com","id":23,"name":"Warren Buffet","updated_at":"2012-05-07T01:49:28Z"},{"created_at":"2012-05-07T01:50:32Z","email":"xrxs@gmail.com","id":24,"name":"Xerxes","updated_at":"2012-05-07T01:50:32Z"},{"created_at":"2012-05-07T01:51:47Z","email":"yanni@aol.com","id":25,"name":"Yanni","updated_at":"2012-05-07T01:51:47Z"},{"created_at":"2012-05-07T01:52:14Z","email":"lightning@hotmail.com","id":26,"name":"Zeus","updated_at":"2012-05-07T01:52:14Z"},{"created_at":"2012-05-07T01:47:00Z","email":"radiohead@me.com","id":20,"name":"Thom Yorke","updated_at":"2012-05-07T01:55:57Z"}] 

Make a note of the Etag and Last Modified headers in the response above.

Issue a conditional GET request with the Etag

Using the etag noted above, you can now make the same request but provide a conditional GET header called If-None-Match.

curl -i -H "If-None-Match: \"ba598025146eff37e26c9150b180a78b\"" http://cache-tester.herokuapp.com/contacts.json

Make sure to escape the inner quotes surrounding the etag

You should get a 304 in response, with no content.

Issue a conditional GET request with the Last Modified Date

Depending on the scenario, it may make sense to use the last modified date instead of the etag (most clients will just support both). To do this, you'll send an If-Modified-Since header with the UTC date you noted from above.

curl -i -H "If-Modified-Since: Mon, 07 May 2012 01:55:57 GMT" http://cache-tester.herokuapp.com/contacts.json

Again, you should receive a 304.

When the data changes

Note that if you make a change to one of the contacts in the system, you will no longer receive a 304 and will have to make a note of the new Etag and Last Modified Date.

Using NSURLCache and SDURLCache

NSURLCache is built-in, however does not support disk caching on iOS 4. If you want your content to persist across application launches, then you'll want to leverage disk caching.

NSURL Class Reference

SDURLCache is a drop in replacement for NSURLCache but does save to disk on iOS 4 and above.

The API above returns a 10 second Cache-control header, which is below the 5 minute threshold that SDURLCache considers reasonable to cache to disk. In order to see this disk caching work for such a small cache duration, you'll have to set a property called minCacheInterval, which accepts an NSTimeInterval in seconds:

- (void)prepareCache {
    SDURLCache *cache = [[SDURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024
                                                      diskCapacity:20 * 1024 * 1024
                                                          diskPath:[SDURLCache defaultCachePath]];
    cache.minCacheInterval = 0;
    [NSURLCache setSharedURLCache:cache];
    NSLog(@"Cache is being logged to: %@", [SDURLCache defaultCachePath]);
}

How NSURLCache is used

If you've set up your NSURLCache (or SDURLCache) then any NSURLConnection based request should automatically leverage the cache. If you need to control this, for example to prevent caching a large file that you know won't be requested again, then you'll have to implement the protocol method:

- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse {
  // can return a customized NSCachedURLResponse or nil to prevent caching for this response
  return nil;
}

If you're using AFNetworking, which wraps these classes, you'll have to set the cacheResponseBlock property that has the same signature:

[operation setCacheResponseBlock:^NSCachedURLResponse *(NSURLConnection *connection, NSCachedURLResponse *cachedResponse) {
    // handle here and return NSCachedURLResponse (or nil)
    return nil;
}];
Want more? Subscribers can view all 573 episodes. New episodes are released regularly.

Subscribe to get access →

Source Code

View on GitHub Download Source