Most of the iPhone apps that I work on don't exist in a vacuum.  That is, they require data that exists somewhere on the server.  Grabbing data from the server is seemingly simple, but you have to dig deeper if you want a truly seamless experience for the user.

Consider the naive approach:

  • App Starts up
  • App Fetches http://someserver/foo.xml
  • App updates the UI with fresh data

If we do this then the server is pinged every single time the app loads.  There are numerous problems with this approach.  The user notices a delay every time they use the app, and most of the time you're loading the same data over and over again.  What happens if the user is offline?

"Aha!" You say.  Let's just enable caching.  That's not a terrible idea.  So you go down the route of implementing a naive caching strategy:  After fetching a resource from the server, cache it on disk so that you don't load it again.

Side note:  TTURLRequest from the Three20 Library does this out of the box.

Now what happens?

  • App Starts up
  • App Fetches http://someserver/foo.xml
  • App updates the UI with fresh data
  • User quits app
  • User launches app again
  • App Loads data from local cache
  • App updates the UI with cached data

Our 2nd launch is improved immensely, and now the app even works offline!  Yipee!  What could be wrong now?  Well now we have another problem:  If we update data on the server, the clients won't know about it and will continue to server stale data to the UI.  At what point should you invalidate the cache?  Hourly?  Daily?  Weekly?

To answer that you really have to examine your domain.  For one of our clients, they have data that fits all over this spectrum:

data-change-frequency

Not much data falls on the left side of this chart, but occasionally you find some that matches.  US States for example might be categorized as "Never Changes".   This app in question is about sports stats, so stats for past seasons literally never change.  They are recorded in history.

On the flip side, this app also shows stats up to the minute as a sports match is in progress.

Clearly we can't use the naive caching approach to serve both of these needs.

Pulling our your HTTP Hat

If you have control over the server and the client, you can take advantage of an excellent-but-not-so-well-known HTTP header called  IF-MODIFIED-SINCE.

This is how the technique would flow:

if-mod-since

With this scenario, we can make a request to the server very quickly, and if nothing has been changed then we simply serve up the file we have cached locally.  We can also follow this same process for offline scenarios (just use cached data).

Using a descriptor file

Unfortunately, we don't always have access to have the server respond to such a header.  Another option is to utilize a small file that is fetched at the beginning.  We call it meta.xml, and it has the filenames & dates of when the content last changed.  This still requires at least 1 network connection at the beginning, but after that, cached data will stay on the phone's local file system until it is proven to be out of date by meta.xml.

That's great, but my app still feels sluggish!

iPhone apps have to have a significant amount of code written in order to make sure that applications are continually responsive (that means NO BLOCKING CODE ON THE MAIN THREAD!).  All network operations should happen on a background thread.

Update:  A friend mentioned that perhaps not ALL network operations need to happen on a background thread... you can utilize non-blocking APIs for some scenarios.  Check out Jeff LaMarche's detailed post on the topic for more information.

In the event that cached data exists, but is proven to be out of date via one of the methods above, we can still bind the UI & show some data to the user so they aren't staring at a blank screen.    We can then kick off a thread to fetch the updated data and notify the UI when updated data has arrived.

activity-update-and-fetch

You can see that this scenario is far more complicated than your simple fetch the data and update.  We must use background processing and callbacks in order to keep the UI responsive.  We immediately return cached data if it exists, so the user can be looking at a screen right away.  Before returning, however, the repository kicks off a background thread to load the data.  When the data comes back, the Repository sends a message to its delegate:

  • `didUpdateWithData:`
  • `noUpdateNeeded`
  • `updateFailedWithError:`

Armed with this architecture, we can build a UI that loads fresh data from the server only when needed, is responsive to user interaction during the remote call to fetch updated data, and will even work offline.

As with most things in software development (and in life)...  doing it the easy way is easy, but doing it the right way takes some time, thought, and effort.