In the past, one of the key differences between native iPhone apps and Web apps was the ability that native apps had to work with local and remote data, whereas iPhone Web apps were limited to working only when a live connection was available. However, Safari on iPhone has embraced support for HTML 5's offline capabilities, enabling you to create Web apps that work even when the user has no access to the Internet.
In this chapter, I'll walk you through these offline capabilities.
Safari on iPhone takes advantage of HTML 5's offline application cache to enable you to pull down remote files from a Web server and store them in a local cache.
In this way, when the device is not connected to the Internet, either through 3G or Wifi access, users can continue to work with your Web app, just in offline mode.
You can include any file in the manifest that can be displayed locally without server-side processing — images (JPG, PNG, and GIF), HTML files, CSS style sheets, and JavaScript scripts.
Safari then attempts to download files in the manifest. If successful, Safari looks for these files in the cache before going to the server. However, in the event of a missing file, incorrect URL, or other error, the update process fails and no further files are downloaded. Then, the next time the manifest is loaded, Safari attempts to download all files once again.
Once Safari downloads the files in a manifest file, the cache is only updated in the future if the manifest file changes. (Note that the content of the manifest file is what is evaluated to determine whether to update the cache, not its last saved date or any other file attribute.) However, if you want to force an update, you can do so programmatically through JavaScript.
To enable the offline cache, you need to create a manifest file that lists each of the files you want to have available offline. The manifest file is an ordinary text file, without HTML or XML markup, that includes two parts:
Declaration: The manifest is declared by typing the following on the first line of the file:
CACHE MANIFEST
URL listings: The lines that follow list the URLs for each file that you want to cache. The paths must be relative to the path of the manifest file.
You can also add comments to the file by adding a # to the start of each line.
Here's a sample manifest file used for a small Web app. Note that this app caches a subset of iUI resources as part of the local cache:
CACHE MANIFEST # images jackarmitage.png # in-use iUI files ../iui/iui.css ../iui/cui.css ../iui/iui.js ../iui/whiteButton.png ../iui/toolButton.png ../iui/toolbar.png ../iui/toggleOn.png ../iui/toggle.png ../iui/thumb.png ../iui/selection.png ../iui/prev.png ../iui/pinstripes.png ../iui/next.png ../iui/loading.gif ../iui/listGroup.png ../iui/listArrowSel.png ../iui/listArrow.png ../iui/grayRow.png ../iui/grayButton.png ../iui/cancel.png ../iui/blueButton.png ../iui/blackToolButton.png ../iui/blackToolbar.png ../iui/blackButton.png ../iui/backButton.png
Once the manifest file is created, save it with a .manifest
extension. For example, in my example, I named it prospector.manifest
.
For offline cache to work correctly, you need to be sure that your Web server serves up the manifest file correctly. Because HTML 5 offline cache is still "cutting-edge" technology, many ISPs do not provide built-in support for it. Therefore, check to make sure your server assigns the MIME type text/cache-manifest
to the manifest
extension. In my case, I had to add it as a custom MIME type.
After you have created the manifest file and uploaded it to your server, you need to link it to your Web app. To do so, add the manifest
attribute to the root html
tag of your Web file:
<html manifest="prospector.manifest" xmlns="http://www.w3.org/1999/xhtml">
In this case, the prosector.manifest
file is in the same directory as the index.html
file.
You have access to the cache using the JavaScript object window.applicationCache
from inside your Web app. To force a cache update, you can use the update()
method. Once the update is complete, you can swap out the old cache with the new cache using the swapCache()
method.
However, before you begin this update process, check to make sure that the application cache is ready for updating. To do so, check the status
property of the applicationCache
object. This property returns one of the values shown in Table 11-1.
Table 11-1. applicationCache.status
Values
Constant | Number | Description |
---|---|---|
|
| No cache is available. |
|
| The local cache is up to date. |
|
| The manifest file is being checked for changes. |
|
| Safari is downloading changed files and has added them to the cache. |
|
| The new cache is ready for updates and to override your existing cache. |
|
| The cache is obsolete and is being deleted. |
For example:
if (window.applicationCache.status == window.applicationCache.UNCACHED) { alert("Houston, we have a cache problem"); } else if (window.applicationCache.status == window.applicationCache.IDLE) { alert("No changes are necessary."); } else { alert("Let's do something with the cache."); }
You can assign event handlers to the applicationCache
object based on the results of the update process. For example:
var localCache = window.applicationCache; localCache.addEventListener("updateready", cacheUpdateReadyHandler, false); localCache.addEventListener("error", cacheErrorHandler, false);
Following are the applicationCache
events that are supported:
checking
error
noupdate
downloading
updateready
cached
obsolete
Because I am focused on programmatically performing an update, I will listen for the updateready
and error
events:
// Handler when local cache is ready for updates function cacheUpdateReadyHandler() { } // Handler for cache update errors function cacheErrorHandler() { alert("Houston, we have a cache problem"); }
Once these event handlers are defined, you are ready to start the update and swap process.
// Handler when local cache is ready for updates function cacheUpdateReadyHandler()
{ localeCache.update(); localeCache.swapUpdate(); }
The update()
method updates the cache, and swapUpdate()
replaces the old cache with the new cache you just successfully downloaded.
When you use application cache, you may have some online processing that you want to disable if you are in offline mode. You can check the connection status in your Web app by checking the navigator
object's onLine
property:
if (navigator.onLine) alert("Online. All services available.") else alert("Offline. Disabling currency rate updates.");
The following examples demonstrate the application cache in action. Listing 11-1 shows the index.html
file, and Listing 11-2 provides a listing of the cacheme.manifest
file. The localeCache
variable is assigned to the window.applicationCache.
Handlers are assigned to applicationCache
events, which determine whether to show a progress indicator of the cache updating process. If the updateready
event is triggered, the swapUpdate()
method is called to update to cache.
Example 11-1. index.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html manifest="cacheme.manifest" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0" /> <title>Cache Me If You Can</title> <script type="text/javascript" language="JavaScript" > // Assign var to applicationCache var localCache = window.applicationCache; // Show progress indicator localeCache.addEventListener("progress", cacheProgressHandler, false);
// Swap cache localeCache.addEventListener("updateready", cacheUpdateReadyHandler, false); // Hide progress indicator localeCache.addEventListener("cached", cacheNoUpdateHandler, false); localeCache.addEventListener("noupdate", cacheNoUpdateHandler, false); // Show error msg localeCache.addEventListener("error", cacheErrorHandler, false); // Called on load function init() { if (navigator.onLine) document.getElementById("onlineIndicator").textContent = "online"; else document.getElementById("onlineIndicator").textContent = "offline"; localeCache.update(); } // Called when no updates are needed function cacheNoUpdateHandler() { document.getElementById("progressSpinner").style.display = "none"; } // Called when a cache update is in progress function cacheProgressHandler() { document.getElementById("progressSpinner").style.display = "table"; } // Called when cache is ready to be updated function cacheUpdateReadyHandler() { localCache.swapCache(); document.getElementById("progressSpinner").style.display = "none"; } // Error handler function cacheErrorHandler() { document.getElementById("progressSpinner").style.display = "none";
alert("A problem occurred trying to load the cache."); } </script> </head> <body onload="init()"> <div id="progressSpinner" style="display:none;"> <p>Loading...<img src="spinner.gif" alt="Loading..." width="16" height="16" /></p> </div> <div id="content"> <div id="onlineIndicator">online|offline</div> <p>This is a test of the Safari on iPhone applicationCache.</p> <img src="boy.png" /> </div> </body> </html>
Figures 11-1 and 11-2 show this example run in both online and airport modes.
In addition to local cache, Safari on iPhone supports HTML 5 key-value storage as a way to provide persistent storage on the client either permanently or within a given browser session. Key-value storage bears several obvious similarities to cookies in client-side storage. However, although cookies are sent back to the server, saved key-value data is not unless you explicitly do so through JavaScript. You also have greater control over the data persistence and window access to that data using key-value storage.
You can specify whether the key values you are saving should be long- or short-term by working with two different JavaScript objects: localStorage
and sessionStorage
.
Use localStorage
when you want to store a key-value pair permanently across browser sessions and windows.
Use sessionStorage
to store temporary data within a given browser window and session.
You can save a key value in one of two ways. First, you can call the setItem()
method on the localStorage
or sessionObject
object.
localStorage.setItem(keyName, keyValue);
For example, to save a user-inputted value as the firstName
key for long-term storage, use this:
localStorage.setItem("firstName", document.getElementById("first_name").value);
Second, a shortcut for saving a value is to treat the key-value name as an actual property of the localStorage
or sessionStorage
objects. For example, the previous example can also be written as follows:
localStorage.firstName = document.getElementById("first_name").value);
However, if you use this shortcut method, you need to make sure that the name of your key-value is a valid JavaScript token.
Any local database is going to have a maximum capacity, so it is good practice to trap for a possible exception in case the capacity has been reached. To do so, trap for QUOTA_EXCEEDED_ERR
. For example:
try { localStorage.firstName = document.getElementById("first_name").value); } catch (error) { if (e == QUOTA_EXCEEDED_ERR) alert("Unable to save first name to the database."); }
Whenever you interact with the local storage database, a storage
event is dispatched from the body
object. Therefore, to handle this event, you can attach a listener to the body
:
document.body.addEventListener("storage", storageHandler, false);
The event object that is passed to the handler enables you to get at various pieces of the transaction (see Table 11-2). For example, the following handler outputs the storage event details to an alert box:
function storageHandler(event) { var info = "A storage event occurred [" + "url=" + event.url + ", " + "key=" + event.key + ", " + "new value=" + event.newValue + ", " + "old value=" + event.oldValue + ", " + "window=" + event.window + "]"; alert(info); }
Table 11-2. storage event
Properties
Property | Description |
---|---|
| URL of the page that calls the storage object. Returns |
| Specifies the key that has been added, changed, or removed. |
| Provides the new value for the key. |
| Provides the old value for the key. If no key was previously defined, |
| Reference to the |
Data can be loaded from the localStorage
and sessionStorage
objects by calling the getItem()
method:
varkeyValue =
sessionStorage.getItem(keyName
);
For example:
var accessCode = sessionStorage.getItem("accessCode");
Or, as you would expect, you can access a key value by calling it as a direct property of the respective object:
var accessCode = sessionStorage.accessCode;
If the key-value that you request is not located, a null
value is returned:
if (sessionStorage.accessCode != null) { var valid = processCode(sessionStorage.accessCode); }
You can remove a specific key-value pair or clear all keys from the local key-value database.
To remove a specific key-value pair, use removeItem()
:
localStorage.removeItem(keyName);
To remove all keys, use one or both of the following:
// Remove all permanent keys localStorage.clear() // Remove all session keys sessionStorage.clear()
The following example shows how you can use HTML 5's key-value storage mechanisms to save permanent and temporary values. An input element value is saved as a permanent key-value pair, whereas the selection in a select
list is saved for the current session only. I want to save the values of these elements each time they change, so I will assign onchange
handlers to both of these elements:
<div> <p>Define a key value in which you want to save permanently:</p> <input id='localValue' onchange="setLocalKeyValue('localValue')"/> <p>Define a key value in which you want to save for this session only:<p> <select id="sessionValue" onchange="setSessionKeyValue('sessionValue')"> <option value="UN">(Select State)</option> <option value="MA">Massachusetts</option> <option value="ME">Maine</option> <option value="RI">Rhode Island</option> <option value="VT">Vermont</option> </select> </div>
I also want to add a button to clear the key-values on demand, as well as a status box that acts as a console to output the storage events that are taking place:
<button onclick="clearAll()">Clear All</button><br /> <div id="statusDiv">Start session</div>
Figure 11-3 shows the page in Safari.
With the HTML markup ready, I can look at the JavaScript code needed to power this example. To begin, I want to confirm that key-value storage is available, so I do a test on the localStorage
and sessionStorage
objects:
var localStorageAvail = typeof(localStorage) != "undefined"; var sessionStorageAvail = typeof(sessionStorage) != "undefined";
I can then check one or both of these values prior to attempting to save or load storage data.
To save the value of the input element permanently, I add the following function:
function setLocalKeyValue(value) { if (localStorageAvail) { localStorage.setItem(value, document.getElementById(value).value); setStatus(document.getElementById(value).id + " saved as a local key."); } }
To save the selectedIndex
of the select
element as a session-only key-value pair, use the following function:
function setSessionKeyValue(value) { if (sessionStorageAvail) { sessionStorage.setItem(value, document.getElementById(value) .selectedIndex); setStatus(document.getElementById(value).id + " saved as a session key."); } }
To retrieve the values of these key-value pairs, I add a loading function:
function loadValues() { if (localStorage.localValue) document.getElementById('localValue').value = localStorage.localValue; if (sessionStorage.sessionValue) document.getElementById('sessionValue').selectedIndex = sessionStorage.sessionValue; }
Next, to clear all the local values, a clearStorage()
function is defined to remove key-value pairs from both sessionStorage
and localStorage
objects as well as the UI fields:
function clearStorage() { // Clear local database sessionStorage.clear();
localStorage.clear(); // Clear UI as well document.getElementById('localValue').value = ""; document.getElementById('sessionValue').selectedIndex = 0; }
Listing 11-3 shows the full source code for the HTML file, which includes additional functions and event handlers to tie everything together.
Example 11-3. index.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0" /> <title>Value Keys</title> <script language="JavaScript" type="text/javascript"> // Check to see whether or not key-value storage is available var localStorageAvail = typeof(localStorage) != "undefined"; var sessionStorageAvail = typeof(sessionStorage) != "undefined"; // Assign event listeners window.addEventListener("onload", init, false); window.addEventListener("onbeforeunload", beforeUnloadHandler, false); // Called on load function init() { // If no storage is available if (!localStorageAvail || !sessionStorageAvail) setStatus("Key value storage is not supported."); // Otherwise, assign handler and retrieve any // previously stored values else { document.body.addEventListener("storage", storageHandler, false); loadValues(); } } // Save local key
// value = id of the element whose value is being saved function setLocalKeyValue(value) { if (localStorage) { localStorage.setItem(value, document.getElementById(value).value); setStatus(document.getElementById(value).id + " saved as a local key."); } } // Save session key // value = id of the element whose value is being saved function setSessionKeyValue(value) { if (sessionStorage) { sessionStorage.setItem(value, document.getElementById(value).selectedIndex); setStatus(document.getElementById(value).id + " saved as a session key."); } } // Loads key-value pairs from local storage function loadValues() { // Is localValue defined? If so, then assign its // value to the text box. if (localStorage.localValue) document.getElementById('localValue').value = localStorage.localValue; // Is sessionValue defined? If so, then assign its // value as the index to the sessionValue select. if (sessionStorage.sessionValue) document.getElementById('sessionValue') .selectedIndex = sessionStorage.sessionValue; } // Clear all key-value pairs function clearStorage() { // Clear local database sessionStorage.clear(); localStorage.clear(); // Clear UI as well document.getElementById('localValue').value = ""; document.getElementById('sessionValue'). selectedIndex = 0;
} // Save current state function saveChanges() { setLocalValue('localValue'), setSessionKeyValue("sessionValue"); // Used to return to the window return null; } // Be sure to save changes before closing a window function beforeUnloadHandler() { return saveChanges(); } // Listener for all storage events. Outputs to the status div. function storageHandler(event) { var info = "A storage event occurred [" + "url=" + event.url + ", " + "key=" + event.key + ", " + "new value=" + event.newValue + ", " + "old value=" + event.oldValue + ", " + "window=" + event.window + "]"; setStatus(info); } // Utility function that outputs specified text to the status // div function setStatus(statusText) { var para = document.createElement("p"); para.appendChild(document.createTextNode(statusText)); document.getElementById("statusDiv").appendChild(para); } </script> </head> <body onload="init()"> <div> <p>Define a key value in which you want to save permanently:</p> <input id='localValue' onchange="setLocalKeyValue('localValue')"/> <p>Define a key value in which you want to save for this session only:</p> <select id="sessionValue" onchange="setSessionKeyValue ('sessionValue')"> <option value="UN">(Select State)</option>
<option value="MA">Massachusetts</option> <option value="ME">Maine</option> <option value="RI">Rhode Island</option> <option value="VT">Vermont</option> </select> </div> <p> <button onclick='clearAll()'>Clear All</button><br /> </p> <div id="statusDiv">Start session</div> </body> </html>
The following figures demonstrate this example when run. Figure 11-4 shows the data being entered on-screen. Figure 11-5 is what's shown after pressing the Refresh button within the current session. As you can see, both values are retained. However, after closing out that window and calling the URL again, the session value is cleared, as shown in Figure 11-6.
When you are working with key-value storage, I recommend using the Web Inspector that is provided with the Windows and Mac versions of Safari (accessed from the Develop menu). The Databases panel (see Figure 11-7) enables you to see the current state of the key-value pairs for the page you are working with.
Once you begin to develop more substantial Web applications, you may easily have local storage needs that go beyond simple caching and key-value pair persistence. Perhaps your app stores application data locally and periodically synchs with a database on a backend server. Or maybe your Web app uses a local database as its sole data repository. Using HTML 5's database capabilities, you can access a local SQLite relational database right from JavaScript to create tables, add and remove records, and perform queries.
The SQLite database is an SQL relational database. For full details on how to work with SQL to create, edit, and query your data, see Robert Vieira's Beginning Microsoft SQL Server 2008 Programming (Wrox, 978-0-470-25701-2).
Your first step is to open a database by calling the openDatabase()
method of the window
object:
var db = window.openDatabase(dbName, versionNum, displayName, maxSize
);
The dbName
parameter is the database name stored locally, versionNum
is its version number, displayName
is the display name of the database used by the browser (if needed), and maxSize
is the maximum size in bytes that the database will be. (Additional size requires user confirmation.) For example:
var db = window.openDatabase("customers", "1.0", "Customer database", 65536);
Once the database is opened, you can perform various tasks on it.
Each task you perform must be part of a transaction. A database transaction can include one or multiple SQL statements and is set up as follows:
db.transaction(transactionFunction, errorCallbackFunction,
successCallbackFunction);
Suppose you wanted to perform a simple query on a customer table in your database. You could set up the query inside of a transaction, such as what is shown here:
var db = window.openDatabase("customers", "1.0", "Customer database", 65536); if (db != null) { var updateSqlStr = "SELECT * FROM customers"; db.transaction( function(transaction) { transaction.executeSql (updateSqlStr) }, successHandler, errorHandler); } function successHandler(result) { if (result.rows.length > 0) { for (var i = 0; i < result.rows.length; i++) { var r = results.rows.item(i); alert(r["first_name"] + " was retrieved from the database."); } } } function errorHandler(error) { alert("An error occurred when trying to perform a query."); }
In this example, the db.transaction
calls an SQL execute statement to return all the customers from the database. The successHandler()
function is called when the database is successful. The errorHandler()
function is called if the operation fails.
The result object returned contains a rows array that contains each record in the returning set. You can access each of the fields by specifying its field name inside the brackets.
Safari on iPhone provides full support for HTML 5's offline capabilities. As a result, you can now create Web apps that can work even when the user does not have direct access to the Internet. In this chapter, I showed you how to work with Safari's offline capabilities. I began by explaining the offline cache and how to configure one through a manifest file for your Web app. The chapter continued with a discussion of key-value storage, enabling you to store both permanent and temporary user data in a way that goes far beyond traditional cookie storage. Finally, I introduced you to how you can access an SQLite relational database from within JavaScript.