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.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
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"; $dir = new RecursiveDirectoryIterator("."); foreach(new RecursiveIteratorIterator($dir) as $file) { if ($file->IsFile() && $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); } } 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: 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?
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 values var 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 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
manifestURL
newestCache
This 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 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.
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.