Chapter 6. Going Offline

If you know HTML, CSS, and JavaScript, you already have what you need to develop your own iPhone apps. With this book, you'll learn how to use these open source web technologies to design and build apps for both the iPhone and iPod Touch. Buy the print book or ebook or purchase the iPhone App.

There’s a feature of HTML5 called the offline application cache that allows users to run web apps even when they are not connected to the Internet. It works like this: when a user navigates to your web app, the browser downloads and stores all the files it needs to display the page (HTML, CSS, JavaScript, images, etc.). The next time the user navigates to your web app, the browser will recognize the URL and serve the files out of the local application cache instead of pulling them across the network.

The Basics of the Offline Application Cache

The main component of the offline application cache is a cache manifest file that you host on your web server. I’m going to use a simple example to explain the concepts involved, and then I’ll show you how to apply what you’ve learned to the Kilo example we’ve been working on.

A manifest file is just a simple text document that lives on your web server and is sent to the user’s device with a content type of cache-manifest. The manifest contains a list of files that a user’s device must download and save in order to function. Consider a web directory containing the following files:

index.html
logo.jpg
scripts/demo.js
styles/screen.css

In this case, index.html is the page that will load into the browser when users visit your application. The other files are referenced from within index.html. To make everything available offline, create a file named demo.manifest in the directory with index.html. Here’s a directory listing showing the added file:

demo.manifest
index.html
logo.jpg
scripts/demo.js
styles/screen.css

Next, add the following lines to demo.manifest:

CACHE MANIFEST
index.html
logo.jpg
scripts/demo.js
styles/screen.css

The paths in the manifest are relative to the location of the manifest file. You can also use absolute URLs, like so:

CACHE MANIFEST
http://www.example.com/index.html
http://www.example.com/logo.jpg
http://www.example.com/scripts/demo.js
http://www.example.com/styles/screen.css

Now that the manifest file is created, you need to link to it by adding a manifest attribute to the HTML tag inside index.html:

<html manifest="demo.manifest">

You must serve the manifest file with the text/cache-manifest content type or the browser will not recognize it. If you are using the Apache web server or a compatible web server, you can accomplish this by adding an .htaccess file to your web directory with the following line:

AddType text/cache-manifest .manifest

Note

If the .htaccess file doesn’t work for you, refer to the portion of your web server documentation that pertains to MIME types. You must associate the file extension .manifest with the MIME type of text/cache-manifest. If your website is hosted by a web hosting provider, your provider may have a control panel for your website where you can add the appropriate MIME type. I’ll also show you an example that uses a PHP script in place of the .htaccess file a little later on in this chapter.

Our offline application cache is now in working order. The next time a user browses to http://example.com/index.html, the page and its resources will load normally over the network. In the background, all the files listed in the manifest will be downloaded to the user’s local disk (or her iPhone’s flash memory). Once the download completes and the user refreshes the page, she’ll be accessing the local files only. She can now disconnect from the Internet and continue to access the web app.

So now that the user is accessing our files locally on her device, we have a new problem: how does she get updates when changes are made to the website?

When the user does have access to the Internet and navigates to the URL of our web app, her browser checks the manifest file on our site to see if it still matches the local copy. If the remote manifest has changed, the browser downloads all the files listed in it. It downloads these in the background to a temporary cache.

Note

The comparison between the local manifest and the remote manifest is a byte-by-byte comparison of the file contents (including comments and blank lines). The file modification timestamp and changes to any of the resources themselves are irrelevant when determining whether or not changes have been made.

If something goes wrong during the download (e.g., the user loses her Internet connection), then the partially downloaded cache is automatically discarded and the previous one remains in effect. If the download is successful, the new local files will be used the next time the user launches the app.

Online Whitelist and Fallback Options

It is possible to force the browser to always access certain resources over the network. This means that the browser will not cache those resources locally, and that they will not be available when the user is offline. To specify a resource as online only, you use the NETWORK: keyword (the trailing : is essential) in the manifest file like so:

CACHE MANIFEST
index.html
scripts/demo.js
styles/screen.css

NETWORK:
logo.jpg

Here, I’ve whitelisted logo.jpg by moving it into the NETWORK section of the manifest file. When the user is offline, the image will show up as a broken image link (Figure 6.1, “Whitelisted images will show up as broken links when the user is offline”). When he is online, it will appear normally (Figure 6.2, “Whitelisted images will show up normally when the user is online”).

Figure 6.1. Whitelisted images will show up as broken links when the user is offline

Whitelisted images will show up as broken links when the user is offline

If you don’t want offline users to see the broken image, you can use the FALLBACK keyword to specify a fallback resource like so:

CACHE MANIFEST
index.html
scripts/demo.js
styles/screen.css

FALLBACK:
logo.jpg offline.jpg

Figure 6.2. Whitelisted images will show up normally when the user is online

Whitelisted images will show up normally when the user is online

Now, when the user is offline, he’ll see offline.jpg (Figure 6.3, “Fallback images will show up when the user is offline”), and when he’s online he’ll see logo.jpg (Figure 6.4, “Hosted images will show up normally when the user is online”).

This becomes even more useful when you consider that you can specify a single fallback image for multiple resources by using a partial path. Let’s say I add an images directory to my website and put some files in it:

/demo.manifest
/index.html
/images/logo.jpg
/images/logo2.jpg
/images/offline.jpg
/scripts/demo.js
/styles/screen.css

Figure 6.3. Fallback images will show up when the user is offline

Fallback images will show up when the user is offline

Figure 6.4. Hosted images will show up normally when the user is online

Hosted images will show up normally when the user is online

I can now tell the browser to fall back to offline.jpg for anything contained in the images directory like so:

CACHE MANIFEST
index.html
scripts/demo.js
styles/screen.css

FALLBACK:
images/ images/offline.jpg

Now, when the user is offline, he’ll see offline.jpg (Figure 6.5, “The same fallback image will show up in place of multiple images when the user is offline”), and when he’s online he’ll see logo.jpg and logo2.jpg (Figure 6.6, “Hosted images will show up normally when the user is online”).

Figure 6.5. The same fallback image will show up in place of multiple images when the user is offline

The same fallback image will show up in place of multiple images when the user is offline

Figure 6.6. Hosted images will show up normally when the user is online

Hosted images will show up normally when the user is online

Whether you should add resources to the NETWORK or FALLBACK section of the manifest file depends on the nature of your application. Keep in mind that the offline application cache is primarily intended to store apps locally on a device. It’s not really meant to be used to decrease server load, increase performance, and so on.

In most cases you should be listing all of the files required to run your app in the manifest file. If you have a lot of dynamic content and you are not sure how to reference it in the manifest, your app is probably not a good fit for the offline application cache and you might want to consider a different approach (a client-side database, perhaps).

Creating a Dynamic Manifest File

Now that we’re comfortable with how the offline app cache works, let’s apply it to the Kilo example we’ve been working on. Kilo consists of quite a few files, and manually listing them all in a manifest file would be a pain. Moreover, a single typo would invalidate the entire manifest file and prevent the application from working offline.

To address this issue, we’re going to write a little PHP file that reads the contents of the application directory (and its subdirectories) and creates the file list for us. Create a new file in your Kilo directory named manifest.php and add the following code:

<?php
  header('Content-Type: text/cache-manifest');1
  echo "CACHE MANIFEST\n";2

  $dir = new RecursiveDirectoryIterator(".");3
  foreach(new RecursiveIteratorIterator($dir) as $file) {4
    if ($file->IsFile() &&5
        $file != "./manifest.php" &&
        substr($file->getFilename(), 0, 1) != ".")
    {
      echo $file . "\n";6
    }
  }
?>

1

I’m using the PHP header function to output this file with the cache-manifest content type. Doing this is an alternative to using an .htaccess file to specify the content type for the manifest file. In fact, you can remove the .htaccess file you created in the section called “The Basics of the Offline Application Cache”, if you are not using it for any other purpose.

2

As you saw earlier in this chapter, the first line of a cache manifest file must be CACHE MANIFEST. As far as the browser is concerned, this is the first line of the document; the PHP file runs on the web server, and the browser only sees the output of commands that emit text, such as echo.

3

This line creates an object called $dir, which enumerates all the files in the current directory. It does so recursively, which means that if you have any files in subdirectories, it will find them, too.

4

Each time the program passes through this loop, it sets the variable $file to an object that represents one of the files in the current directory. In English, this line would read: “Each time through, set the file variable to the next file found in the current directory or its subdirectories.”

5

The if statement here checks to make sure that the file is actually a file (and not a directory or symbolic link). It also ignores files named manifest.php or any file that starts with a . (such as .htaccess).

Note

The leading ./ is part of the file’s full path; the . refers to the current directory and the / separates elements of the file’s path. So there’s always a ./ that appears before the filename in the output. However, when I check for a leading . in the filename I use the getFilename function, which returns the filename without the leading path. This way, I can detect files beginning with . even if they are buried in a subdirectory.

6

Here’s where I display each file’s name.

To the browser, manifest.php will look like this:

CACHE MANIFEST
./index.html
./jqtouch/jqtouch.css
./jqtouch/jqtouch.js
./jqtouch/jqtouch.transitions.js
./jqtouch/jquery.js
./kilo.css
./kilo.js
./themes/apple/img/backButton.png
./themes/apple/img/blueButton.png
./themes/apple/img/cancel.png
./themes/apple/img/chevron.png
./themes/apple/img/grayButton.png
./themes/apple/img/listArrowSel.png
./themes/apple/img/listGroup.png
./themes/apple/img/loading.gif
./themes/apple/img/on_off.png
./themes/apple/img/pinstripes.png
./themes/apple/img/selection.png
./themes/apple/img/thumb.png
./themes/apple/img/toggle.png
./themes/apple/img/toggleOn.png
./themes/apple/img/toolbar.png
./themes/apple/img/toolButton.png
./themes/apple/img/whiteButton.png
./themes/apple/theme.css
./themes/jqt/img/back_button.png
./themes/jqt/img/back_button_clicked.png
./themes/jqt/img/button.png
./themes/jqt/img/button_clicked.png
./themes/jqt/img/chevron.png
./themes/jqt/img/chevron_circle.png
./themes/jqt/img/grayButton.png
./themes/jqt/img/loading.gif
./themes/jqt/img/on_off.png
./themes/jqt/img/rowhead.png
./themes/jqt/img/toggle.png
./themes/jqt/img/toggleOn.png
./themes/jqt/img/toolbar.png
./themes/jqt/img/whiteButton.png
./themes/jqt/theme.css

Note

Try loading the page yourself in a browser (be sure to load it with an HTTP URL such as http://localhost/~YOURUSERNAME/manifest.php). If you see a lot more files in your listing, you may have some extraneous files from the jQTouch distribution. The files LICENSE.txt, README.txt, and sample.htaccess are safe to delete, as are the directories demos and extensions. If you see a number of directories named .svn, you may also safely delete them, though they will not be visible in the Mac OS X Finder (you can work with them from within the Terminal, however).

Now open index.html and add a reference manifest.php like so:

<html manifest="manifest.php">

Now that the manifest is generated dynamically, let’s modify it so that its contents change when any of the files in the directory change (remember that the client will redownload the application only if the manifest’s contents have changed). Here is the modified manifest.php:

<?php
  header('Content-Type: text/cache-manifest');
  echo "CACHE MANIFEST\n";

  $hashes = "";1

  $dir = new RecursiveDirectoryIterator(".");
  foreach(new RecursiveIteratorIterator($dir) as $file) {
    if ($file->IsFile() &&
        $file != "./manifest.php" &&
        substr($file->getFilename(), 0, 1) != ".")
    {
      echo $file . "\n";
      $hashes .= md5_file($file);2
    }
  }
  echo "# Hash: " . md5($hashes) . "\n";3
?>

1

Here, I’m initializing a string that will hold the hashed values of the files.

2

On this line I’m computing the hash of each file using PHP’s md5_file function (Message-Digest algorithm 5), and appending it to the end of the $hashes string. Any change to the file, however small, will also change the results of the md5_file function. The hash is a 32-character string, such as “4ac3c9c004cac7785fa6b132b4f18efc”.

3

Here’s where I take the big string of hashes (all of the 32-character strings for each file concatenated together), and compute an MD5 hash of the string itself. This gives us a short (32 characters, instead of 32 multiplied by the number of files) string that’s printed out as a comment (beginning with the comment symbol #).

From the viewpoint of the client browser, there’s nothing special about this line. It’s a comment, and the client browser ignores it. However, if one of the files is modified, this line will change, which means the manifest has changed.

Here’s an example of what the manifest looks like with this change (some of the lines have been truncated for brevity):

            CACHE MANIFEST
./index.html
./jqtouch/jqtouch.css
./jqtouch/jqtouch.js
...
./themes/jqt/img/toolbar.png
./themes/jqt/img/whiteButton.png
./themes/jqt/theme.css
# Hash: ddaf5ebda18991c4a9da16c10f4e474a

The net result of all of this business is that changing a single character inside of any file in the entire directory tree will insert a new hash string into the manifest. This means that any edits we do to any Kilo files will essentially modify the manifest file, which in turn will trigger a download the next time a user launches the app. Pretty nifty, eh?

Debugging

It can be tough to debug apps that use the offline application cache because there’s very little visibility into what is going on. You find yourself constantly wondering if your files have downloaded, or if you are viewing remote or local resources. Plus, switching your device between online and offline modes is not the snappiest procedure and can really slow down the develop, test, debug cycle.

There are two things you can do to help determine what’s going on when things aren’t playing nice: set up some console logging in JavaScript, and browse the application cache database.

Note

If you want to see what’s happening from the web server’s perspective, you can monitor its logfiles. For example, if you are running a web server on a Mac computer, you can open a Terminal window (ApplicationsUtilitiesTerminal) and run these commands (the $ is the Terminal shell prompt and should not be typed):

$ cd /var/log/apache2/
$ tail -f access_log

This will display the web server’s log entries, showing information such as the date and time a document was accessed, as well as the name of the document. When you are done, press Control-C to stop following the log.

The JavaScript Console

Adding the following JavaScript to your web apps during development will make your life a lot easier, and can actually help you internalize the process of what is going on. The following script will send feedback to the console and free you from having to constantly refresh the browser window (you can store the script in a .js file that your HTML document references via the script element’s src attribute):

// Convenience array of status values1
var cacheStatusValues = [];
cacheStatusValues[0] = 'uncached';
cacheStatusValues[1] = 'idle';
cacheStatusValues[2] = 'checking';
cacheStatusValues[3] = 'downloading';
cacheStatusValues[4] = 'updateready';
cacheStatusValues[5] = 'obsolete';

// Listeners for all possible events2
var cache = window.applicationCache;
cache.addEventListener('cached', logEvent, false);
cache.addEventListener('checking', logEvent, false);
cache.addEventListener('downloading', logEvent, false);
cache.addEventListener('error', logEvent, false);
cache.addEventListener('noupdate', logEvent, false);
cache.addEventListener('obsolete', logEvent, false);
cache.addEventListener('progress', logEvent, false);
cache.addEventListener('updateready', logEvent, false);

// Log every event to the console
function logEvent(e) {3
    var online, status, type, message;
    online = (navigator.onLine) ? 'yes' : 'no';
    status = cacheStatusValues[cache.status];
    type = e.type;
    message = 'online: ' + online;
    message+= ', event: ' + type;
    message+= ', status: ' + status;
    if (type == 'error' && navigator.onLine) {
        message+= ' (prolly a syntax error in manifest)';
    }
    console.log(message);4
}

// Swap in newly downloaded files when update is ready
window.applicationCache.addEventListener(
    'updateready', 
    function(){
        window.applicationCache.swapCache();
        console.log('swap cache has been called');
    }, 
    false
);

// Check for manifest changes every 10 seconds
setInterval(function(){cache.update()}, 10000);

This might look like a lot of code, but there really isn’t that much going on here:

1

The first seven lines are just me setting up an array of status values for the application cache object. There are six possible values defined by the HTML5 spec, and here I’m mapping their integer values to a short description (e.g., status 3 means “downloading”). I’ve included them to make the logging more descriptive down in the logEvent function.

2

In the next chunk of code, I’m setting up an event listener for every possible event defined by the spec. Each one calls the logEvent function.

3

The logEvent function takes the event as input and makes a few simple calculations in order to compose a descriptive log message. Note that if the event type is error and the user is online, there is probably a syntax error in the remote manifest. Syntax errors are extremely easy to make in the manifest because all of the paths have to be valid. If you rename or move a file but forget to update the manifest, future updates will fail.

4

Once I have my message composed, I send it to the console.

You can view the console messages in desktop Safari by selecting DevelopShow Error Console. You can view the console messages in the iPhone Simulator by going to SettingsSafariDeveloper and turning the Debug Console on. When debugging is turned on, Mobile Safari displays a header above the location bar (Figure 6.7, “Mobile Safari with debugging turned on”) that allows you to navigate to the debugging console (Figure 6.8, “Mobile Safari debugging console”).

Figure 6.7. Mobile Safari with debugging turned on

Mobile Safari with debugging turned on

Figure 6.8. Mobile Safari debugging console

Mobile Safari debugging console

Note

If you don’t see the Develop menu in the Safari menu bar, open your Safari application preferences, click the Advanced tab, and make sure that “Show Develop menu in menu bar” is checked.

If you load the web page in your browser and then open the console, you’ll see new messages appear every 10 seconds (Figure 6.9, “The console.log() function can be used to send debugging messages to the JavaScript console”). If you don’t see anything, update the version number in demo.manifest and reload the page in your browser twice. I strongly encourage you to play around with this until you really have a feel for what’s going on. You can tinker around with the manifest (change the contents and save it, rename it, move it to another directory, etc.) and watch the results of your actions pop into the console like magic.

Figure 6.9. The console.log() function can be used to send debugging messages to the JavaScript console

The console.log() function can be used to send debugging messages to the JavaScript console

The Application Cache Database

If you are having serious trouble debugging your offline web app, there is a way to get under the hood and see what’s going on. If you load your app in the iPhone Simulator, it stores the cached resources in a SQLite database that you can peruse with the sqlite3 command-line interface. Of course, having some knowledge of SQL would help here, but you can get pretty far by mimicking the examples in this section.

Note

You will need to install the iPhone SDK from Apple in order to get the simulator. You can get the SDK by registering as an Apple developer at http://developer.apple.com/iphone/. Registration costs nothing, but you will need to enroll in an iPhone developer program (note that an Apple developer is different from an iPhone developer) if you want to submit your apps to the App Store.

On my machine, the iPhone Simulator app cache database is located here:

/Users/jstark/Library/Application Support/iPhone
Simulator/User/Library/Caches/com.apple.WebAppCache/ApplicationCache.db

Note

The com.apple.WebAppCache directory and ApplicationCache.db database will not exist unless you have loaded the web application on the iPhone Simulator at least once.

Using the sqlite3 command-line interface, you can poke around in the database to get an idea of what’s going on. First, you have to connect to the database. Open the Terminal (ApplicationsUtilitiesTerminal) and type the commands that follow. (The $ is the Terminal prompt and should not be typed.)

$ cd "$HOME/Library/Application Support/iPhone Simulator"
$ cd User/Library/Caches/com.apple.WebAppCache/
$ sqlite3 ApplicationCache.db

Note

On the Mac, desktop Safari’s application cache can be found in a directory adjacent to your temporary directory. You can get to it in the terminal with:

$ cd $TMPDIR/../-Caches-/com.apple.Safari/
$ sqlite3 ApplicationCache.db

Once connected, you’ll see something like:

SQLite version 3.6.17
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite>

Now you can type SQLite control statements and arbitrary SQL commands at the sqlite> prompt. To see a list of SQLite control statements, type .help at the prompt. You’ll see a long list of commands, of which these are the most important for our purposes:

.exit                  Exit this program
.header(s) ON|OFF      Turn display of headers on or off
.help                  Show this message
.mode MODE ?TABLE?     Set output mode where MODE is one of:
                         csv      Comma-separated values
                         column   Left-aligned columns.  (See .width)
                         html     HTML <table> code
                         insert   SQL insert statements for TABLE
                         line     One value per line
                         list     Values delimited by .separator string
                         tabs     Tab-separated values
                         tcl      TCL list elements
.quit                  Exit this program
.tables ?PATTERN?      List names of tables matching a LIKE pattern

To retrieve a list of tables used in the cache manifest database, use the .tables command:

sqlite> .tables
CacheEntries        CacheResourceData   CacheWhitelistURLs  FallbackURLs
CacheGroups         CacheResources      Caches

Before I start querying the tables, I’m going to set .headers to ON, which will add field names to the output, and set .mode to line to make things easier to read. Type the commands shown in bold (sqlite> is the SQLite prompt):

sqlite> .headers on
sqlite> .mode line

CacheGroups is the top level of the data model. It contains a row for each version of the manifest. Type the command shown in bold (don’t forget the ;):

sqlite> select * from CacheGroups;
              id = 1
manifestHostHash = 2669513278
     manifestURL = http://jonathanstark.com/labs/kilo10/kilo.manifest
     newestCache = 7

              id = 2
manifestHostHash = 2669513278
     manifestURL = http://jonathanstark.com/labs/cache-manifest-bug/test.manifest
     newestCache = 6

              id = 5
manifestHostHash = 2669513278
     manifestURL = http://jonathanstark.com/labs/kilo11/kilo.manifest
     newestCache = 13

              id = 6
manifestHostHash = 2669513278
     manifestURL = http://jonathanstark.com/labs/app-cache-3/demo.manifest
     newestCache = 14

As you can see, I have four cache groups on my machine. You probably only have one at this point. The fields break down like this:

id

A unique autoincrement serial number assigned to the row. Every time Mobile Safari inserts a row into this table, this number is incremented. If, for some reason, Mobile Safari needs to delete a row, you will see gaps in the sequence.

manifestHostHash

Used with manifestURL to uniquely identify the cache.

manifestURL

The location of the remote manifest file.

newestCache

This is a Caches row ID (i.e., a foreign key to the Caches table) that indicates which cache to use.

Note

A column in a database table is considered a key when it identifies something. For example, a unique key identifies a row in the table unambiguously. A primary key is a unique key that has been designated as the key you use to identify a row. For example, two columns are potential unique keys because there is only one row in the CacheGroups table for any given value of these columns: id and manifestURL. However, id is a simple numeric key, and it’s very fast to make comparisons to it (and it requires less storage for other tables to refer to it). So, id is both a unique key and the primary key for the CacheGroups table.

A foreign key is a link from one table to another. The cacheGroup column in the Caches table (discussed next) identifies a row in the CacheGroups table, establishing a link from a row in one table to the other.

Now, switch to column mode and select all rows from the Caches table:

sqlite> .mode column
sqlite> select * from Caches;
id          cacheGroup
----------  ----------
6           2         
7           1         
13          5         
14          6

The Caches table has just two fields: id (primary key for the Caches row), and cacheGroup (foreign key that links a Caches id to a row in the CacheGroups table). If Safari were in the process of downloading a new cache, there would be two Cache rows for the CacheGroup (one current, one temporary). In all other cases, there is only one Cache row per CacheGroup.

Next, let’s select all of the rows from the CacheEntries table:

sqlite> select * from CacheEntries;
cache       type        resource  
----------  ----------  ----------
6           1           67        
6           4           68        
6           2           69        
7           4           70        
7           4           71        
7           4           72        
7           4           73        
7           2           74        
7           4           75        
7           4           76        
7           4           77        
7           1           78        
7           4           79        
13          4           160       
13          4           161       
13          4           162       
13          4           163       
13          2           164       
13          4           165       
13          4           166       
13          4           167       
13          4           168       
13          1           169       
13          4           170       
13          4           171       
13          4           172       
13          4           173       
13          4           174       
13          4           175       
14          4           176       
14          16          177       
14          4           178       
14          1           179       
14          4           180       
14          2           181

Not much to look at here. Just two foreign keys (cache, which is a foreign key to the Caches.id column, and resource, which is a foreign key to CacheResources.id) and a type field. I’ll redo that query with a join to the CacheResources table so you can see how the type corresponds to the actual files. Notice that first I set the column widths so the URLs don’t get cut off (the ...> prompt indicates that I pressed Return before finishing the statement with the ; terminator):

sqlite> .width 5 4 8 24 80
sqlite> select cache, type, resource, mimetype, url
   ...> from CacheEntries,CacheResources where resource=id order by type;
--  -- --- ----------- --------------------------------------------------------------
6   1  67  text/htm... http://jonathanstark.com/labs/cache-manifest-bug/                               
7   1  78  text/htm... http://jonathanstark.com/labs/kilo10/#home                                      
13  1  169 text/htm... http://jonathanstark.com/labs/kilo11/#home                                      
14  1  179 text/htm... http://jonathanstark.com/labs/app-cache-3/                                      
6   2  69  text/cac... http://jonathanstark.com/labs/cache-manifest-bug/test.manifest                  
7   2  74  text/cac... http://jonathanstark.com/labs/kilo10/kilo.manifest                              
13  2  164 text/cac... http://jonathanstark.com/labs/kilo11/kilo.manifest                              
14  2  181 text/cac... http://jonathanstark.com/labs/app-cache-3/demo.manifest                         
6   4  68  image/pn... http://jonathanstark.com/labs/kilo10/icon.png                                   
7   4  70  text/css... http://jonathanstark.com/labs/kilo10/jqtouch/jqtouch.css                        
7   4  71  image/pn... http://jonathanstark.com/labs/kilo10/icon.png                                   
7   4  72  text/css... http://jonathanstark.com/labs/kilo10/themes/jqt/theme.css                       
7   4  73  image/pn... http://jonathanstark.com/labs/kilo10/startupScreen.png                          
7   4  75  applicat... http://jonathanstark.com/labs/kilo10/jqtouch/jqtouch.js                         
7   4  76  applicat... http://jonathanstark.com/labs/kilo10/kilo.js                                    
7   4  77  applicat... http://jonathanstark.com/labs/kilo10/jqtouch/jquery.js                          
7   4  79  image/x-... http://jonathanstark.com/favicon.ico                                            
13  4  160 applicat... http://jonathanstark.com/labs/kilo11/kilo.js                                    
13  4  161 text/css... http://jonathanstark.com/labs/kilo11/jqtouch/jqtouch.css                        
13  4  162 image/pn... http://jonathanstark.com/labs/kilo11/icon.png                                   
13  4  163 image/x-... http://jonathanstark.com/favicon.ico                                            
13  4  165 image/pn... http://jonathanstark.com/labs/kilo11/themes/jqt/img/button.png                  
13  4  166 image/pn... http://jonathanstark.com/labs/kilo11/themes/jqt/
img/chevron.png                 
13  4  167 text/css... http://jonathanstark.com/labs/kilo11/themes/jqt/theme.css                       
13  4  168 applicat... http://jonathanstark.com/labs/kilo11/jqtouch/jquery.js                          
13  4  170 applicat... http://jonathanstark.com/labs/kilo11/jqtouch/jqtouch.js                         
13  4  171 image/pn... http://jonathanstark.com/labs/kilo11/themes/jqt/
img/back_button.png             
13  4  172 image/pn... http://jonathanstark.com/labs/kilo11/themes/jqt/img/toolbar.png                 
13  4  173 image/pn... http://jonathanstark.com/labs/kilo11/startupScreen.png                          
13  4  174 image/pn... http://jonathanstark.com/labs/kilo11/themes/jqt/
img/back_button_clicked.png     
13  4  175 image/pn... http://jonathanstark.com/labs/kilo11/themes/jqt/
img/button_clicked.png          
14  4  176 text/htm... http://jonathanstark.com/labs/app-cache-3/index.html                            
14  4  178 applicat... http://jonathanstark.com/labs/app-cache-3/scripts/demo.js                       
14  4  180 text/css... http://jonathanstark.com/labs/app-cache-3/styles/screen.css                     
14  16 177 image/jp... http://jonathanstark.com/labs/app-cache-3/images/offline.jpg

Reviewing this list reveals that type 1 indicates a host file, type 2 is a manifest file, type 4 is any normal static resource, and type 16 is a fallback resource.

Let’s switch back to line mode and pull some data from the CacheResources table to see what is going on in there. Here’s resource row 73 (if you’re trying this out yourself, replace 73 with a valid id value from the results you got in the previous query of the CacheResources table):

sqlite> .mode line
sqlite> select * from CacheResources where id=73;
              id = 73
             url = http://jonathanstark.com/labs/kilo10/startupScreen.png
      statusCode = 200
     responseURL = http://jonathanstark.com/labs/kilo10/startupScreen.png
        mimeType = image/png
textEncodingName = 
         headers = Date:Thu, 24 Sep 2009 19:16:09 GMT
X-Pad:avoid browser bug
Connection:close
Content-Length:12303
Last-Modified:Fri, 18 Sep 2009 05:02:26 GMT
Server:Apache/2.2.8 (Fedora)
Etag:"52c88b-300f-473d309c45c80"
Content-Type:image/png
Accept-Ranges:bytes

            data = 73

If you are familiar with the way HTTP requests work, you’ll recognize that this is exactly the data that you’d need to fake a network response. Here Mobile Safari has all the info needed to serve up a PNG file to the browser (or in this case, to itself; it is storing the information needed to reproduce the behavior of the web server that originally provided the file).

Well, in fact it has all of the info except for the actual image data. The image data is stored in a blob field in CacheResourceData. I’d include it here, but it’s binary and not much to look at. It’s interesting to note that even text datafiles (HTML, CSS, JavaScript, etc.) and the like are stored as binary data in the blob field in CacheResourceData.

Let’s take a look at the CacheWhitelistURLs table, which contains all the elements identified in the NETWORK: section of the manifest:

sqlite> .width 80 5
sqlite> .mode column
sqlite> select * from CacheWhitelistURLs;
url                                                                           cache
---------------------------------------------------------------------------- ------
http://jonathanstark.com/labs/kilo10/themes/jqt/img/back_button.png           7    
http://jonathanstark.com/labs/kilo10/themes/jqt/img/back_button_clicked.png   7    
http://jonathanstark.com/labs/kilo10/themes/jqt/img/button.png                7    
http://jonathanstark.com/labs/kilo10/themes/jqt/img/button_clicked.png        7    
http://jonathanstark.com/labs/kilo10/themes/jqt/img/chevron.png               7    
http://jonathanstark.com/labs/kilo10/themes/jqt/img/toolbar.png               7

Here we just have the cache id and the URL to the online resource. If cache id 7 is requested by the browser, these six images will be retrieved from their remote location if the user is online. If the user is offline, they will show up as broken links because they are not stored locally. It’s worth noting that the URLs have been fully expanded to absolute URLs, even though they were listed in the manifest as relative URLs.

And finally, let’s take a look at the FallbackURLs table (everything from the FALLBACK: section of the manifest):

sqlite> .mode line
sqlite> select * from FallbackURLs;
  namespace = http://jonathanstark.com/labs/app-cache-3/images/
fallbackURL = http://jonathanstark.com/labs/app-cache-3/images/offline.jpg
      cache = 14

As you can see, I currently have only one row in the FallbackURLs table. If cache id 14 is requested by the browser, and any URLs that begin with http://jonathanstark.com/labs/app-cache-3/images/ fail for whatever reason (the user is offline, images are missing, etc.), the fallbackURL will be used instead.

I apologize if this section is a bit complex, but at this point it’s all we’ve got. Maybe browser vendors will implement some sort of user interface that will allow us to browse the application cache—similar to those for the local storage and client-side database—but until that time comes, this is our only option for prowling around in the depths of client-side storage.

What You’ve Learned

In this chapter, you’ve learned how to give users access to a web app, even when they have no connection to the Internet. This offline mode applies whether the app is loaded in Mobile Safari, or launched in full screen mode from a Web Clip icon on the desktop. With this new addition to your programming toolbox, you now have the ability to create a full-screen, offline app that is virtually indistinguishable from a native application downloaded from the App Store.

Of course, a pure web app such as this is still limited by the security constraints that exist for all web apps. For example, a web app can’t access the Address Book, the camera, the accelerometer, or vibration on the iPhone. In the next chapter, I’ll address these issues and more with the assistance of an open source project called PhoneGap.

Site last updated on: November 17, 2010 at 10:11:32 AM PST