Chapter 11. Offline Applications

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.

The HTML 5 Offline Application Cache

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.

Creating a Manifest File

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.

Referencing the Manifest File

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.

Programmatically Controlling the Cache

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

window.applicationCache.UNCACHED

0

No cache is available.

window.applicationCache.IDLE

1

The local cache is up to date.

window.applicationCache.CHECKING

2

The manifest file is being checked for changes.

window.applicationCache.DOWNLOADING

3

Safari is downloading changed files and has added them to the cache.

window.applicationCache.UPDATEREADY

4

The new cache is ready for updates and to override your existing cache.

window.applicationCache.OBSOLETE

5

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.

Checking the Connection Status

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.");

Putting It All Together

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>

Example 11-2. cacheme.manifest

CACHE MANIFEST

#  images
boy.png
spinner.gif

Figures 11-1 and 11-2 show this example run in both online and airport modes.

Running in online mode

Figure 11-1. Running in online mode

Running in offline mode still works and displays manifest resources

Figure 11-2. Running in offline mode still works and displays manifest resources

Using Key-Value Storage

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.

Saving a Key-Value

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

URL of the page that calls the storage object. Returns null if the requesting page is not in the same window. Returns undefined if calling from a local page.

key

Specifies the key that has been added, changed, or removed.

newValue

Provides the new value for the key.

oldValue

Provides the old value for the key. If no key was previously defined, null is returned.

window

Reference to the window object that called the storage object. Returns null if the requesting page is not in the same window.

Loading Key-Value Data

Data can be loaded from the localStorage and sessionStorage objects by calling the getItem() method:

var keyValue = 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);
}

Deleting Key-Value Data

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()

Putting It All Together

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.

Page ready to save data

Figure 11-3. Page ready to save data

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.

Values being stored as key-value pairs

Figure 11-4. Values being stored as key-value pairs

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.

Refreshing the page in the current session retains both permanent and temporary values.

Figure 11-5. Refreshing the page in the current session retains both permanent and temporary values.

A new browser session shows just the permanent key-value.

Figure 11-6. A new browser session shows just the permanent key-value.

Interact with key-value pairs in the Databases panel of the Safari Web Inspector

Figure 11-7. Interact with key-value pairs in the Databases panel of the Safari Web Inspector

Going SQL with the JavaScript Database

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).

Opening a Database

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);

Querying a Table

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.

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.116.15.161