iPhone/iPod TouchのSafariでオフラインアプリケーション

以前iPod Touchを買ったのですが、普段ほとんど外に持ち出さず、自宅内のWebブラウザとして使ってました。が、ふと(今作っている)時間簿Webアプリケーションの入力インターフェースとして使えるのではないかと思い立ち、どうせやるならということでHTML5APIを使ってオフラインで動くようにできるか試してみました。

時間簿アプリケーションは単にその日の行動を時間ベースで記録してくだけのシンプルなものです。元々携帯向けのWebアプリケーションとして作るつもりだったのですが、最新に近い機種で動かしてみても通信が大分重く、直接スケジュール帳に記載する方が楽だったので、アプリケーションの方はちょっと放置気味でした。

まず、HTML5でオフラインアプリケーションを作成するには、以下のAPIを呼び出すことになると思います。

  • 必須
    • Offline Application Caching API
  • データをブラウザに保存する場合

今回はこれらのAPIの中で「Offline Application Caching API」と「SQL-based database API」の2つを使ってみました。幸いなことに、この2つのAPIiPod TouchSafari上で動作することが確認できました。

Offline Application Caching API

Webアプリケーションは通常、HTMLや画像ファイルをWebサーバからダウンロードします。オフラインで動かす為には、これらをブラウザ内に保持する必要があります。その仕組みが「Offline Application Caching API」で提供されます。

MANIFESTファイルを作成する

ダウンロードするファイルの一覧をMANIFESTファイルに記述します。今回はこのMANIFESTファイルのファイル名を「manifest.mf」とします。

CACHE MANIFEST
# Version 0.1
weekcalendar.html
/js/jquery.weekcalendar.js
/css/jquery.weekcalendar.css
http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/themes/smoothness/jquery-ui.css
http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js
http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/jquery-ui.min.js
Webサーバ側で、MANIFESTファイル用のmime typeを設定する

今回はGAE/Jのサーバに配備するので、web.xmlに以下の記述を追加します。

<mime-mapping>
  <extension>mf</extension>
  <mime-type>text/cache-manifest</mime-type>
</mime-mapping>    
MANIFESTファイルを指定する

ダウンロードするHTMLファイルのHTMLタグのmanifest属性に、ブラウザに保存されるファイル一覧が記載されたMANIFESTファイル名を指定します。

<html manifest="manifest.mf">

以上で、初回のダウンロード以降、サーバが死んでいてもHTMLを表示することができます。

SQL-based database API

ブラウザ内のdatabaseにデータを保存し、更新、削除、およびデータの問い合わせを行うことができます。Firefox3.6はこのAPIをサポートしていないので、Chromeで動きを確認しました。

openDatabase

データベース名を指定して、databaseをオープンします。バージョンや容量を指定することができます。(最大5MB)
バージョンを見て、DBのスキーマ定義を更新する、という仕組みもあるようです。(あまりうまく使えなかったので、後日改めて調べてみます)

var db = openDatabase('lifecycle', '1.0', 'lifecycle', 5242880); 
insert

DB操作を行う場合は、transactionメソッドを呼び出します。transactionメソッドには、DB操作、DB操作失敗時の振る舞い、DB操作成功時の振る舞いの3つのハンドラを渡します。注意する点として、これらのハンドラは非同期で実行されます。ただ、所謂GUIスレッドかどうかを気にする必要はないらしく、これらのハンドラ内でwindow#alert等を呼び出すことができます。DML文はtransactionオブジェクトのexecuteSqlメソッドを使って発行します。

function saveEvent(calEvent){
  var db = openDatabase('lifecycle', '2.0', 'lifecycle', 5242880);
  db.transaction(
    function(t){
      var s = calEvent.start;
      t.executeSql('INSERT INTO cal_events VALUES(NULL, ?, ?, ?, ?, ?, ?, ?)', [s.getFullYear(), s.getMonth(), s.getDate(), s, calEvent.end, calEvent.title, calEvent.body], function(tx, rs){
        calEvent.id = rs.insertId;
      });
    },
    function(error){
      // DB操作失敗時の振る舞い
      alert("(saveEvent)db error:" + error);
    },
    function(){
      // DB操作成功時の振る舞い
      $('#calendar').weekCalendar("updateEvent", calEvent);
      $('#calendar').weekCalendar("removeUnsavedEvents");
    }
  );
}
update

発行するのがupdate文ということ以外、insertと同じです。

function updateEvent(calEvent){
  var db = openDatabase('lifecycle', '2.0', 'lifecycle', 5242880); 
  db.transaction(
    function(t){
      t.executeSql('UPDATE cal_events set start=?, end=?, title=?, body=? WHERE id=?', [calEvent.start, calEvent.end, calEvent.title, calEvent.body, calEvent.id]);
    },
    function(error){
      alert("(updateEvent)db error:" + error.message);
    },
    function(){
      $('#calendar').weekCalendar("updateEvent", calEvent);
      $('#calendar').weekCalendar("removeUnsavedEvents");
    }
  );
}
delete

発行するのがdelete文ということ以外、updateと同じです。

function deleteEvent(calEvent){
  var db = openDatabase('lifecycle', '2.0', 'lifecycle', 5242880);
  db.transaction(
    function(t){
      t.executeSql('DELETE FROM cal_events WHERE id=?', [calEvent.id]);
    },
    function(error){
      alert("(deleteEvent)db error:" + error.message);
    },
    function(){
    }
  );
}
select

select文については、問い合わせた結果をオブジェクトに格納する部分が入ってきます。下記のselect文発行も非同期となる為、selectした結果を以って次の処理に移行するには、DB操作成功時の振る舞いのハンドラ(db.transactionメソッド呼び出しの第3引数)に次の処理を記述することになります。

function loadEvents(newDate, handler){
  var db = openDatabase('lifecycle', '2.0', 'lifecycle', 5242880);
  var eventData = new Array();
  db.transaction(
    function(t){
      t.executeSql('SELECT id, year, month, day, start, end, title, body FROM cal_events WHERE year=? and month=? and day=?', [newDate.getFullYear(), newDate.getMonth(), newDate.getDate()], function(tx, rs){
        for (var i = 0; i < rs.rows.length; i++) {
          var row = rs.rows.item(i);
          eventData[i] = {"id": row.id, "year": row.year, "month": row.month, "day": row.day, "start": new Date(row.start), "end": new Date(row.end), "title": row.title, "body": row.body};
        }
      });
    },
    function(error){
      alert("(loadEvents)db error:" + error.message);
    },
    function(){
      handler(eventData);
    }
  );    
}
スキーマ定義

DB操作を行うページの最初に、スキーマ定義を行う必要があります。ただ、この操作も非同期になる為、呼び出すタイミングはちょっと難しいと考えています。今のところ、ページの最初のクエリ発行前に実行するようにしてますが、もっと適切なタイミングがあるんじゃないかな…。

t.executeSql('CREATE TABLE IF NOT EXISTS cal_events(id INTEGER PRIMARY KEY, year INTEGER, month INTEGER, day INTEGER, start TEXT, end TEXT, title TEXT, body TEXT)');

UI

時間簿をつけるインターフェースは、jquery-week-calendarというモジュールを使わせてもらいました。Google Calendarにかなり近いUIを提供してくれます。jQueryおよびjQuery-UIに依存しており、iPod TouchSafariで見ると動きが非常にモッサリしていますが、表示についてはPCのブラウザと同じように見えます。ただ、エントリを登録する際に、まず開始点をmouse downで指定し、その状態のままdrag→mouse upで終点を指定するようになっているのですが、iPod TouchSafariでdragするとスクロールの方が動いてしまう為、2回目のmouse downで終点を指定するようにしています。

http://github.com/masayuki038/jquery-week-calendar/blob/master/jquery.weekcalendar.js

おわりに

今回試しに作ってみた時間簿は、以下にあります(データを受けるサーバ側はありませんが)。ChromeiPhone/iPod Touchでアクセスしてください(iPhoneは持ってないので試してません)。エントリを操作するダイアログがなかなか表示されなかったり、ドロップダウンリストに時刻が設定されるまでに大分時間がかかります。気長に待ってください。また、デバッグが面倒なのでOffline Application Caching APIは使わないようにしています。

http://life-cycle.appspot.com/weekcalendar.html

アクセスするとデータベースを作成してしまうので、iPod Touchであればメニューから「設定」→「Safari」→「データベース」→「編集」で任意のデータベースを削除することができます。Chromeは「オプション」→「高度な設定」タブ→「コンテンツの設定」→「Cookieとその他のサイトのデータを表示」ボタン押下でドメイン毎にデータベースが表示されるので、任意のデータベースを選択して削除します。