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 Source Code SDURLCache Charles Proxy Sample web app w/ API Heroku Dev Center article on iOS HTTP Caching 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; }];