| 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 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.cssNext, 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
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.
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.
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”).
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
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
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
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).
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'); echo "CACHE MANIFEST\n";
  echo "CACHE MANIFEST\n"; $dir = new RecursiveDirectoryIterator(".");
  $dir = new RecursiveDirectoryIterator("."); foreach(new RecursiveIteratorIterator($dir) as $file) {
  foreach(new RecursiveIteratorIterator($dir) as $file) { if ($file->IsFile() &&
    if ($file->IsFile() && $file != "./manifest.php" &&
        substr($file->getFilename(), 0, 1) != ".")
    {
      echo $file . "\n";
        $file != "./manifest.php" &&
        substr($file->getFilename(), 0, 1) != ".")
    {
      echo $file . "\n"; }
  }
?>
    }
  }
?>| I’m using the PHP  | |
| As you saw earlier in this chapter, the
        first line of a cache manifest file must be  | |
| This line creates an object called
         | |
| Each time the program passes through this
        loop, it sets the variable  | |
| The  | 
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.
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
Try loading the page yourself in a browser
      (be sure to load it with an HTTP URL such as
      http://localhost/~).
      If you see a lot more files in your listing, you may have some
      extraneous files from the jQTouch distribution. The files
      YOURUSERNAME/manifest.phpLICENSE.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 = ""; $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);
  $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); }
  }
  echo "# Hash: " . md5($hashes) . "\n";
    }
  }
  echo "# Hash: " . md5($hashes) . "\n"; ?>
?>| Here, I’m initializing a string that will hold the hashed values of the files. | |
| On this line I’m computing the hash of each
        file using PHP’s  | |
| 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: ddaf5ebda18991c4a9da16c10f4e474aThe 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?
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.
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 (Applications→Utilities→Terminal) 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.
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 valuesvar cacheStatusValues = []; cacheStatusValues[0] = 'uncached'; cacheStatusValues[1] = 'idle'; cacheStatusValues[2] = 'checking'; cacheStatusValues[3] = 'downloading'; cacheStatusValues[4] = 'updateready'; cacheStatusValues[5] = 'obsolete'; // Listeners for all possible events
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) {
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);
} // 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:
| 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  | |
| 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  | |
| The  | |
| Once I have my message composed, I send it to the console. | 
You can view the console messages in desktop Safari by selecting Develop→Show Error Console. You can view the console messages in the iPhone Simulator by going to Settings→Safari→Developer 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”).
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.
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.
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
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
      (Applications→Utilities→Terminal) 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
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 patternTo retrieve a list of tables used in the cache manifest database, use the .tables command:
sqlite> .tables
CacheEntries        CacheResourceData   CacheWhitelistURLs  FallbackURLs
CacheGroups         CacheResources      CachesBefore 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 = 14As 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:
idA 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.
manifestHostHashmanifestURLnewestCacheThis is a Caches row ID (i.e., a
            foreign key to the
            Caches table) that indicates which cache to
            use.
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           181Not 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.
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.