小さな Titanium Mobile の読み物

The Little Book on Titanium (Appcelerator Titanium)

JavaScript を使って iOS/Android などマルチプラットフォーム対応のネイティブアプリケーションを構築することができる Appcelerator Titanium についてまとめた非公式サイトです。

ページ1 | はじめに

  1. Appcelerator Titanium
  2. Write Once, Adapt Anywhere
  3. 使えるの?
  4. 苦手なものは何?

ページ2 | 開発ツール

  1. Titanium Studio
  2. Titanium CLI

ページ3 | 開発スタイル

  1. JavaScript
  2. Titanium Classic
  3. Alloy

ページ4 | アプリケーションの例

  1. プロジェクトを作る
  2. レイアウトを考える
  3. 土台を作る
  4. 細かい機能を実装していく
  5. 終わりに

ページ5 | Titanium を拡張する

  1. モジュール
  2. Titanium Cloud Services
  3. altJS

アプリケーションの例

シンプルな Todo アプリケーションを作ってみましょう。 Todo アプリの要件は

  1. いつまでに
  2. 何を行うか

を管理できるものとします。今回の例では Alloy を使った開発の例を紹介します。

プロジェクトを作る

まずは Titanium Studio でプロジェクトを作ります。 File → New → Mobile App Project を選択して、新しいプロジェクトの作成画面を表示させてください。

新しいプロジェクトの作成画面

Project Template は Alloy の Default Alloy Project を選択します。

Project Template

Project Location は以下のように設定します。

App Id はもしも独自ドメインをお持ちの場合は、ドメインを TLD から逆順に並べ、最後に Project name を小文字にしたものを付加すると良いでしょう。 Company/Personal URL は App Id を正順にしたものを入力します。

Cloud-enable this applications はチェックを外します。このチェックは Titanium Cloud Services を使う場合にチェックを入れます。必要に応じて後から有効にもできますので、ここではチェックを外してプロジェクトを作成します。

Project Location

プロジェクトの作成を終えると Titanium Studio は作成したプロジェクトを選択した状態になります。今回は Android 4.0 以降と iOS 6 以降を対象にアプリケーションを作ってみます。これでプロジェクトのひな形ができあがりました。このひな形を元に、 Todo アプリを構築していきます。

初期状態

レイアウトを考える

アプリケーションのレイアウトを考えます。 Titanium は iOS や Android などのマルチプラットフォームに対応したネイティブアプリケーションを作ることができるツールです。1つのコードを元に、複数のプラットフォームに対応するということは、プラットフォームそれぞれが提供する UI の作法に従う必要があります。

例えば、ウィンドウを切り替えるタブを使う場合、 iOS では画面の下に表示され、 Android では画面の上に表示されます。同じ使い勝手を実現する為に、同じ UI を実現することは正しい選択肢ではありません。ユーザが最も慣れ親しんでいる UI にフィットさせることが多くの場合最適な選択になります。

今回は下図のように2つのタブで「これから行うタスク」と「行ったタスク」を切り替えて表示できるアプリケーションを構築していきます。

UI の違い

土台を作る

Alloy は見た目を XML で定義します。まずは2つのタブを持つ画面を作ります。 app/views/index.xml を以下のように編集します。

<Alloy>
  <TabGroup>
    <Tab title="Tasks">
      <Window title="Tasks"></Window>
    </Tab>
    <Tab title="Done">
      <Window title="Done"></Window>
    </Tab>
  </TabGroup>
</Alloy>

Titanium では TabGroup というものを使ってタブを管理します。タブの中には Window を入れます。ウィンドウの背景色を白にしてみましょう。見た目に関する情報は TSS を編集します。 index.xml に対応する TSS は app/styles/index.tss です。

"Window": {
  backgroundColor: "#FFFFFF"
}

TSS は非常に便利な機能で、特定の要素で指定したい見た目を一括定義することができます。この例では index.xml 中の Window 要素全てに対して backgroundColor: "#FFFFFF" を割り当てました。これで先に掲載している図と同じ見た目になります。

タスクを追加するためのボタンを用意する

タスクを追加するためのボタンを用意しておきます。ボタンは iOS ではナビゲーションバーの左右に配置することができます。 Android 3.0 以降は ActionBar に ActionButton を設置可能です。

index.xml
<Alloy>
  <TabGroup onOpen="tabOpen">
    <Tab title="Tasks">
      <Window title="Tasks">
        <RightNavButton platform="ios">
          <Button title="Add" onClick="addTask" id="addButton" />
        </RightNavButton>
        <!-- Android 向けの ActionBar のメニューはコントローラ側に書く -->
      </Window>
    </Tab>
    <Tab title="Done">
      <Window title="Done"></Window>
    </Tab>
  </TabGroup>
</Alloy>
index.tss
"Window": {
  backgroundColor: "#FFFFFF"
}
"#addButton[platform=ios]": {
  systemButton: Ti.UI.iPhone.SystemButton.COMPOSE
}
index.js
function addTask() {
  alert('Add Task Function');
}

function tabOpen(e) {
  // Android だったらアクションバーにメニューを表示する 
  if (OS_ANDROID) {
    var activity = $.index.getActivity();
    activity.onCreateOptionsMenu = function (e2) {
      var menuItem = e2.menu.add({
        title: 'Add',
        icon: '/images/ic_action_edit.png',
        showAsAction: Ti.Android.SHOW_AS_ACTION_IF_ROOM
      });
      menuItem.addEventListener('click', addTask);
    };
    activity.invalidateOptionsMenu();
  }
}

$.index.open();

新しい記述が沢山登場しました。 index.xml の Window タグの中では RightNavButton タグが登場しました。RightNavButton タグには platform="" 属性が付いています。ここでは iosandroid など、プラットフォーム名を値にします。ここでは iOS 専用なので ios を値にしています。さらに中では Button タグを使っています。

同様の動きを Android にも持たせたいところですが、 TabGroup タグを使っている場合、 XML 中にメニューを書くことができません。個人的には XML 中に記述したいところですが、 Titanium の仕様上、 TabGroup を使っているときはコントローラ中で動的にメニューを作る必要があります。

index.tss の中では #addButton 指定と [platform=] 疑似クラスが登場しました。 TSS の要素指定で # から始めるものは XML 中の id 属性を割り当てた要素にヒモづけられます。また [platform=] 疑似クラスを使うことで、プラットフォーム毎の要素指定が可能です。 iOS では systemButton を使い、システムに用意されている作成ボタンを使用します。

index.js の中では addTask 関数と tabOpen 関数を作りました。 addTask 関数は呼び出されると alert 関数によって "Add Task Function" というメッセージが表示されます。この関数の名前は XML 中の onClick 属性の値とヒモづけられます。今回の例では Button タグに onClick="addTask" 属性を記述していますので、 Button をクリックすると、この関数が呼び出されます。

また、 tabOpen 関数は XML 中の TabGroup の onOpen="tabOpen" 属性にヒモづけられています。関数中では if (OS_ANDROID) という条件文が現れていて、 Android のときだけ処理したいときに記述します。 Alloy では OS_IOS などのプリセットされた条件を使うことができます。条件文中で Android であれば、 TabGroup からアクティビティを取得し、 onCreateOptionMenuinvalidateOptionsMenu の2つを使ってメニューを作っています。 menuItem 変数には addEventListener を使ってクリックイベントを割り当てていて、クリックすると addTask 関数が呼び出されます。

新しいコントローラ

コードを分割する

このまま作業を継続しても良いのですが、既に index.xml が長くなってきました。それぞれのタブが持つウィンドウを別のコントローラに切り出して、コードを分割してみましょう。 Alloy では見た目の分離を行うために Require というタグが用意されています。これを使うために、新しくコントローラを作ります。

controllers を右クリックして、new → Alloy Controller を選択してください。

新しいコントローラ

Controller name にはコントローラの名前を入力します。ここでは TasksDone の2つのコントローラを作ってください。

コントローラの作成

コントローラを作ると controllers / styles / views それぞれのディレクトリに入力した名前のファイルが作られます。 index.xml で Require タグを使って参照するビューを指定します。以下のように編集してください。

<Alloy>
  <TabGroup onOpen="tabOpen">
    <Tab title="Tasks" id="tasksTab">
      <Require type="view" src="Tasks" />
    </Tab>
    <Tab title="Done" id="doneTab">
      <Require type="view" src="Done" />
    </Tab>
  </TabGroup>
</Alloy>

Window タグを Require タグで置き換えました。 Require タグでは src 属性を使って参照するビューを決定しています。参照しているビューを編集する必要があります。 Tab の中にはウィンドウを入れる必要がありますので、 app/views/Tasks.xmlapp/views/Done.xml を以下のように編集します。

Tasks.xml
<Alloy>
  <Window title="Tasks">
    <RightNavButton platform="ios">
      <Button title="Add" onClick="addTask" id="addButton" />
    </RightNavButton>
  </Window>
</Alloy>
Done.xml
<Alloy>
  <Window title="Done"></Window>
</Alloy>

ここまで編集した状態でシミュレータなどを使って動作確認を行うと分かりますが、 index.tss で指定していたウィンドウの背景色が適応されていません。 Alloy では Require を使って分割した場合、参照先のビューと対応する TSS で見た目を構築する必要があります。もしも、アプリケーション全体で本当に共通にしたい見た目がある場合、 styles ディレクトリの中に app.tss を作ってください。

app/styles/app.tss - 一例
"Window": {
  backgroundColor: "#FFFFFF"
}

同様に、 index.tss で定義していた、 Button に関するスタイルも適応されませんので、 Tasks.tss を編集します。

app/styles/Tasks.tss
"#addButton[platform=ios]": {
  systemButton: Ti.UI.iPhone.SystemButton.COMPOSE
}

また、 index.js に記述していた addTask 関数も Tasks.js に移動します。このとき、 index.js にあった addTask 関数を menuItem.addEventListener 中で参照できなくなるため、 Tasks コントローラのオブジェクトを Alloy.createController で作り、このコントローラのメソッドとして呼び出すように変更します。

Tasks.js 中に移動した addTask 関数は $.addTask = addTask という新しい行が追加されています。これは Tasks コントローラのメソッドとして addTask を公開するという意味です。これによって、 index.js 中から Tasks コントローラのメソッドとして addTask 関数を呼び出すことができます。

index.js
function tabOpen(e) {
  // Android だったらアクションバーにメニューを表示する 
  if (OS_ANDROID) {
    var activity = $.index.getActivity();
    activity.onCreateOptionsMenu = function (e2) {
      var menuItem = e2.menu.add({
        title: 'Add',
        icon: '/images/ic_action_edit.png',
        showAsAction: Ti.Android.SHOW_AS_ACTION_IF_ROOM
      });

      // Tasks コントローラのオブジェクトを作り、
      // そのメソッドとして addTask を呼び出す
      var tasksController = Alloy.createController('Tasks');
      menuItem.addEventListener('click', tasksController.addTask);
    };
    activity.invalidateOptionsMenu();
  }
}

$.index.open();
Tasks.js
function addTask() {
  alert('Add Task Function');
}
// Tasks コントローラのメソッドとして addTask 関数を登録する
$.addTask = addTask;

タスクを表示するためのビューを準備する

Titanium では情報を列挙するための仕組みとして TableView と ListView の2種類が用意されています。 TableView は以前から存在するポピュラーなビューで、 TableViewRow (行) 1つ1つに様々な子ビューを設置し、個別に内容を書き換えたり、イベントを処理したりします。

Titanium 3.1 で登場した ListView は情報を決まった形でフォーマットして列挙する、データ駆動型のビューです。列挙方法をテンプレートという形で定義しておき、情報をテンプレートに当てはめて列挙します。 ListView は TableView よりもパフォーマンスが高く、 Android では特に顕著です。ただし、 TableView のように行の中身の要素を個別に書き換えたり、様々なイベントを処理するような表示には向いていません。

今回の Todo アプリでは 伝統的な TableView を使います。 Tasks.xmlDone.xml を以下のように編集してください。 TableView タグを Window タグの中に記述し、 id 属性を付けます。

Tasks.xml
<Alloy>
  <Window title="Tasks">
    <RightNavButton platform="ios">
      <Button title="Add" onClick="addTask" id="addButton" />
    </RightNavButton>
    <Menu platform="android">
      <MenuItem title="Add" onClick="addTask" id="addButton" />
    </Menu>
    <TableView id="tasks"></TableView>
  </Window>
</Alloy>
Done.xml
<Alloy>
  <Window title="Done">
    <TableView id="done"></TableView>
  </Window>
</Alloy>

これで土台ができあがりました。肉付けを行っていきましょう。

細かい機能を実装していく

タスク入力ウィンドウ

詳細部分を実装してきましょう。まずはタスクの追加と表示です。タスクは Button または MenuItem を押したときにタスクを追加するウィンドウを表示させ、そこで入力を行うものとしましょう。

まずは追加ボタンを押したときに新しいウィンドウを開く機能から実装していきます。 Add コントローラを作成してください。コントローラを作成したら Add.xml を以下のように編集します。

Add.xml
<Alloy>
  <Window id="addWin" title="Add Task" tabBarHidden="true">
  </Window>
</Alloy>

XML はウィンドウを定義していますが、新しく tabBarHidden="true" 属性が付いています。ウィンドウを開いたときにタブを切り替えるためのタブバーを非表示にするためのオプションです。

次に、タスク追加ボタンを押したときに、ウィンドウ開かれるようにします。 Tasks.js の addTask 関数を以下のように変更します。また、 index.xmlindex.js を編集します。

Tasks.js
function addTask() {
  var addWin, index;
  if (Alloy.Globals.currentTab === undefined) {
    index = Alloy.createController("index");
    Alloy.Globals.currentTab = index.getView("tasksTab");
  }
  addWin = Alloy.createController("Add").getView("addWin");
  Alloy.Globals.currentTab.open(addWin);
}
index.xml
<Alloy>
  <TabGroup onOpen="tabOpen" onFocus="tabFocus">
    <Tab title="Tasks" id="tasksTab">
      <Require type="view" src="Tasks" />
    </Tab>
    <Tab title="Done" id="doneTab">
      <Require type="view" src="Done" />
    </Tab>
  </TabGroup>
</Alloy>
index.js - 追加・編集
.....
      // Tasks コントローラのオブジェクトを作り、
      // そのメソッドとして addTask を呼び出す
      var tasksController = Alloy.createController('Tasks');
      menuItem.addEventListener('click', tasksController.addTask);
    };
    activity.invalidateOptionsMenu();
  }

  // TabGroup が開かれたときのタブをグローバルに参照できるようにする
  Alloy.Globals.currentTab = e.activeTab;
}

function tabFocus(e) {
  // タブを切り替えたらそのタブをグローバルに参照できるようにする
  Alloy.Globals.currentTab = e.tab;
}

タブを使ったアプリケーションの場合、現在開いているタブの中で新しいウィンドウを開くことができます。現在開いているタブは Alloy.Globals オブジェクトの currentTab プロパティに参照を持たせます。 index.js の中ではタブを切り替えるたびに開いているタブへの参照を保存するイベントを追加しています。

参照が無い場合は index コントローラから Tasks タブへの参照をもらってきます。 getView メソッドは引数で ID を指定することができるので、指定したビューの特定の要素をコントローラの中から呼び出すことができます。

同様に addWin 変数に Add コントローラから ID "addWin" を持つ要素 (= Window 要素) を取得しています。このウィンドウを参照しているタブの open メソッドに渡して開きます。

モーダルウィンドウ

次にタスク入力画面のレイアウトを行います。必要な情報は

  1. いつまでに
  2. 何を行うか

ですので、これらの入力画面を作ります。 Add.xml, Add.tss, Add.js をそれぞれ編集します。変更点が多いので、注意してください。

Add.xml
<Alloy>
  <Window id="addWin" title="Add Task" tabBarHidden="true">
    <ScrollView id="addWrap" layout="vertical" onClick="blurTextArea">
      <Label>1. いつまでに</Label>
      <Picker id="todoLimit" />
      <Label>2. 何を行う</Label>
      <TextArea id="inputTask" />
      <Label>3. 保存する</Label>
      <Button id="saveTask" title="保存する" onClick="saveTask" />
    </ScrollView>
  </Window>
</Alloy>
Add.tss
"#addWin[platform=ios]": {
  backgroundColor: "#FFFFFF"
}
"Label": {
  font: {
    fontSize: "16sp",
    fontWeight: "bold"
  },
  textAlign: "left",
  color: "#333333",
  width: Ti.UI.SIZE,
  height: Ti.UI.SIZE,
  top: "11dp",
  bottom: "11dp",
  left: "11dp",
  right: "11dp"
}
"Label[platform=android]": {
  color: "#FFFFFF"
}
"#todoLimit": {
  type: Ti.UI.PICKER_TYPE_DATE,
  locale: "ja"
}
"#inputTask": {
  font: {
    fontSize: "16sp"
  },
  width: Ti.UI.FILL,
  height: "96dp",
  top: "11dp",
  bottom: "11dp",
  left: "11dp",
  right: "11dp"
}
"#inputTask[platform=ios]": {
  borderWidth: 1,
  borderColor: "#CCCCCC"
}
"#saveTask": {
  width: Ti.UI.FILL,
  top: "11dp",
  bottom: "11dp",
  left: "11dp",
  right: "11dp"
}
"#saveTask[platform=ios]": {
  height: '44dp'
}
Add.js
$.todoLimit.minDate = new Date();
function blurTextArea() {
  $.inputTask.blur();
}
function saveTask() {
  Ti.API.info("Save task function");
}

新しく ScrollArea 要素、 Picker 要素、 TextArea 要素が登場しました。 ScrollArea は画面からはみ出すような大きさを持つ要素をスクロールして表示させることができるビューです。 Picker はロール状に表示されたいくつかの項目の中から選択するための UI です。 TextArea は複数行に対応したテキストを入力するための UI です。

ScrollArea では layout 属性を設定しています。値は vertical です。 layout 属性を設定すると、その中に設置した UI パーツの配置順を縦並びにしたり、横並びにしたりすることができます。ここでは vertical を設定していますので、 ScrollView の中に配置した UI パーツは縦に並びます。

TSS ではそれぞれのプラットフォームに適応するスタイルを多く変更しています。単位に注目すると fontSizeheightspdp という単位を使っています。 sp は主に文字のサイズに使い、 dp は画像や UI 要素に使います。この単位は Android では有効で、 iOS では単純な数値として取り扱われます。 Picker のスタイルでは Ti.UI.PICKER_TYPE_DATE を設定しています。年月日を選択する Picker を指定するスタイルです。

入力画面の完成

入力画面が完成しました。次に、タスクを入力して、保存する機能を実装しましょう。

入力と保存

タスクを入力したら「保存する」ボタンを押して保存する機能を実装していきます。 Titanium はいくつかの保存先を使うことができますが、最もポピュラーな保存先は SQLite です。 Titanium Classic を使ったアプリ開発では SQL をコードの中に直接記述していましたが、 Alloy では Model が OR マッパーを提供しているのでモデルの値を SQL を書かずに保存することができます。

まず始めに、期限の Picker を変更した際に年月日の情報を取得する必要があるので、この機能を実装します。 Add.xmlAdd.js を以下のように変更します。

Add.xml
<Alloy>
  <Window id="addWin" title="Add Task" tabBarHidden="true">
    <ScrollView id="addWrap" layout="vertical" onClick="blurTextArea">
      <Label>1. いつまでに</Label>
      <Picker id="todoLimit" onChange="setLimitTime" />
      <Label>2. 何を行う</Label>
      <TextArea id="inputTask" />
      <Label>3. 保存する</Label>
      <Button id="saveTask" title="保存する" onClick="saveTask" />
    </ScrollView>
  </Window>
</Alloy>
Add.js
$.todoLimit.minDate = new Date();
function blurTextArea() {
  $.inputTask.blur();
}
function saveTask() {
  Ti.API.info("Save task function");
}
var limitTime;
function setLimitTime(e) {
  limitTime = (e.value).getTime();
}

Picker に onChange 属性を追加し、 setLimitTime 関数とヒモづけました。 setLimitTime 関数は、 Picker の値が変更されたら、関数の外にある変数 limitTime に日付の情報を保存しています。

次にモデルを定義します。 models を右クリックして、new → Alloy Model を選択してください。

モデルの作成

以下の内容でモデルを定義します。

モデルの定義

これでモデルの定義ができました。 Models ディレクトリの中に Todo.js ができています。この JavaScript ファイルの中では定義したモデルの情報やモデルのバリデータなど、モデルに属した情報を使ったロジックを記述します。

早速モデルを使ってみましょう。 Add.js の saveTask 関数は保存するボタンを押したときに実行される関数です。この関数の中で Todo モデルを作り、 SQLite にタスク情報を保存してみます。 Add.js の saveTask 関数を以下のように変更します。

Add.js - saveTask 関数
function saveTask() {
  var todo = Alloy.createModel("Todo", {
    task: $.inputTask.value,
    limitTime: "" + limitTime,
    done: false
  });
  todo.save();
}

todo 変数に Alloy.createModel メソッドを使って新しくモデルオブジェクトを代入します。 createModel メソッドは引数に文字列で定義したモデルの名前を受け取ります。2つめの引数はオブジェクトの形で渡します。オブジェクトの中には、定義したモデルのスキーマ名と値をセットしたものを記述します。

確かにこれでタスク情報の保存はできるのですが、例えば、タスクが入力されていない場合や日付が無効な場合など、適切でない値を設定している場合に保存できてしまってはアプリケーションとして正しくありません。

そこで、バリデータを記述することができます。バリデータはユーザの入力値がアプリケーションにとって正しいものかどうかを判断し、正常な場合のみ保存などの処理を行う機能を提供します。バリデータはモデルの定義ファイルである Todo.js に記述します。 app/models/Todo.js を以下のように編集します。

Todo.js
exports.definition = {
  config: {
    columns: {
      "task": "text",
      "limitTime": "text",
      "done": "integer"
    },
    adapter: {
      type: "sql",
      collection_name: "Todo"
    }
  },
  extendModel: function (Model) {
    _.extend(Model.prototype, {
      validate: function (attr) {
          if ((attr.task).length <= 0) {
            return "Error: Task is not input.";
          }
          if (String(attr.limitTime).length <= 0) {
            return "Error: Limit time is not set.";
          }
        }
    });

    return Model;
  },
  extendCollection: function (Collection) {
    _.extend(Collection.prototype, {
      // extended functions and properties go here
    });

    return Collection;
  }
};

validate メソッドを追加しました。このメソッドはコントローラ中で作ったモデルオブジェクトから、 isValid メソッドとして呼び出すことができます。このバリデータを使うために Add.js の saveTask 関数を以下のように変更します。

Add.js - saveTask 関数
function saveTask() {
  limitTime = limitTime || Date.now();
  var todo = Alloy.createModel("Todo", {
    task: $.inputTask.value,
    limitTime: "" + limitTime,
    done: 0
  });
  if (todo.isValid()) {
    todo.save();
    $.addWin.close({
      animated: true
    });
    alert("Save!");
    Alloy.Collections.Todo.fetch();
  } else {
    todo.destroy();
    alert("Failed");
  }
}

モデルオブジェクト todo の isValid メソッドを呼んでいます。このメソッドを呼び出すと Todo.js 中で定義した validate メソッドを呼び出します。もしも想定外の値が返却されてきた場合は、 else 文の中に処理が移ります。 else の中ではモデルオブジェクトを破棄し、保存失敗を示すために alert を表示します。

保存失敗

何かしらのタスクを入力し、日付を設定した後「保存する」ボタンを押すことで、バリデータをパスしますが、このままだと Alloy.Collections.Todo.fetch() の呼び出しに失敗してしまいます。 app/alloy.js を修正しましょう。

alloy.js
Alloy.Collections.Todo = Alloy.createCollection("Todo");

alloy.js は index.js よりも前に呼び出され、アプリケーション全体で使用するオブジェクトやコレクションを準備するためのファイルです。 Todo モデルに対応する Todo コレクションを作り、これを Alloy.Collections.Todo に格納しています。

この状態でタスクを入力し、保存を押すことで保存が正常に終了することを確認してください。

タスクの入力

保存成功

タスクの表示

登録したタスク情報を表示しましょう。 SQLite に登録した情報を表示する方法はいくつかありますが、 Alloy の特徴的な機能である Data Binding を使ってみましょう。 Data Binding は保存したモデルの情報の集合であるコレクションと、データ列挙を行うための TableView / ListView をヒモ付けて、一覧の表示を簡単に実現するものです。

いくつかのファイルを修正します。まずは index.js を修正しましょう。

index.js - 追記
$.index.open();

$.index.addEventListener("close", function () {
  $.destroy();
});

Alloy.Collections.Todo.fetch();

$.index.addEventListener("close")Alloy.Collections.Todo.fetch(); を追加しました。 close イベント中で行っている $.destroy() は公式ドキュメントに記載されている必須項目です。メモリリークを防ぐために使います。

Alloy.Collections.Todo.fetch(); はコレクションと表示の同期を行うためのイベントを発火するメソッドです。こちらも必須です。

次に Tasks.xml を修正します。

Tasks.xml
<Alloy>
  <Window title="Tasks">
    <RightNavButton platform="ios">
      <Button title="Add" onClick="addTask" id="addButton" />
    </RightNavButton>
    <TableView id="tasks" dataCollection="Todo" dataTransform="transData" dataFilter="filterData">
      <TableViewRow onClick="doneConfirm" _id="{alloy_id}">
        <View id="taskWrap">
          <Label class="taskText" id="task" text="{task}" />
          <Label class="taskText" id="limitTime" text="{limitTime}" />
        </View>
      </TableViewRow>
    </TableView>
  </Window>
</Alloy>

<Collection src="Todo" /> の追加と TableView タグの修正、中に TableViewRow 要素と子要素を追加しました。参照するコレクションの指定と Data Binding の表示方法を定義しています。

TableView タグでは dataCollectiondataTransformdataFilter 要素を使っています。これは TableView とヒモづけるコレクションを指定し、表示のためにデータの加工を行う関数を指定します。

TableViewRow 要素では onClick 属性で doneConfirm 関数とヒモづけています。タスクのクリック時にタスクが完了したかどうかを問い合わせるダイアログを表示させます。実装は Tasks.js で行います。また、 _id 属性では {alloy_id} という値を持っています。この {} で表す値はモデルのスキーマを参照するための命令文です。 Alloy はモデルを特定する為の特別なスキーマとして alloy_id というスキーマと持ちます。この値と TableViewRow をヒモづけるために _id 属性を追加しています。

TableViewRow 要素の中では1つのビューと2つのラベルを定義していて、 Label の中では text 要素に {task}{limitTime} を値とし、 Todo モデルの task スキーマと limitTime スキーマを参照します。

Tasks.js
function addTask() {
  var addWin, index;
  if (Alloy.Globals.currentTab === undefined) {
    index = Alloy.createController("index");
    Alloy.Globals.currentTab = index.getView("tasksTab");
  }
  addWin = Alloy.createController("Add").getView("addWin");
  Alloy.Globals.currentTab.open(addWin);
}
// Tasks コントローラのメソッドとして addTask 関数を登録する
$.addTask = addTask;

var moment = require("alloy/moment");
function transData(model) {
  var transform, limitTime;
  transform = model.toJSON();
  limitTime = transform.limitTime;
  transform.limitTime = moment(Number(limitTime)).format("YYYY/MM/DD");
  return transform;
}

function filterData(collection) {
  return collection.where({
    done: 0
  });
}

var dialogs = require("alloy/dialogs");
function doneConfirm(e) {
  dialogs.confirm({
    message: "Done?",
    callback: function() {
      var model = Alloy.Collections.Todo.where({
        alloy_id: e.rowData._id
      })[0];
      model.set({
        done: 1
      }).save();
    }
  });
}

transData 関数は Data Binding のためにモデルオブジェクトの値を加工する関数です。 Alloy にはいくつかのライブラリが組み込まれており、日付関連の処理を簡単にする moment.js もその1つです。 moment.js ライブラリを読み込み、期限情報を YYYY/MM/DD の形に変換しています。

filterData 関数は Data Binding のために表示するデータのフィルタリングを行う関数です。ここでは done: 0 (未完了) のものだけを表示するフィルタを定義しています。

doneConfirm 関数は TableViewRow をクリックしたときにそのタスクが完了したかどうかを訪ね、完了していたら done: 1 (完了) に更新する関数です。ここでも Alloy の dialog ライブラリを使い、確認画面を作っています。モデルの特別なスキーマである alloy_id を元に対応するモデルオブジェクトを作り、 done: 1 に更新した後 save メソッドで保存しています。

表示用のスタイルである Tasks.tss を更新します。

Tasks.tss
"#addButton[platform=ios]": {
  systemButton: Ti.UI.iPhone.SystemButton.COMPOSE
}
"#taskWrap": {
  width: Ti.UI.FILL,
  height: Ti.UI.SIZE,
  top: "6dp",
  right: "11dp",
  bottom: "6dp",
  left: "11dp",
  layout: "horizontal"
}
".taskText": {
  width: Ti.UI.FILL,
  height: Ti.UI.SIZE,
  textAlign: "left"
}
"#task": {
  font: {
    fontSize: "18sp",
    fontWeight: "bold"
  }
}
"#limitTime": {
  font: {
    fontSize: "14sp"
  }
}

同様に Done コントローラも修正します。

Done.xml
<Alloy>
  <Collection src="Todo"/ >
  <Window title="Done">
    <TableView id="done" dataCollection="Todo" dataTransform="transData" dataFilter="filterData">
      <TableViewRow>
        <View id="taskWrap">
          <Label class="taskText" id="task" text="{task}" />
          <Label class="taskText" id="limitTime" text="{limitTime}" />
        </View>
      </TableViewRow>
    </TableView>
  </Window>
</Alloy>
Done.js
var moment = require("alloy/moment");
function transData(model) {
  var transform, limitTime;
  transform = model.toJSON();
  limitTime = transform.limitTime;
  transform.limitTime = moment(Number(limitTime)).format("YYYY/MM/DD");
  return transform;
}

function filterData(collection) {
  return collection.where({
    done: 1
  });
}
Done.tss
"#taskWrap": {
  width: Ti.UI.FILL,
  height: Ti.UI.SIZE,
  top: "6dp",
  right: "11dp",
  bottom: "6dp",
  left: "11dp",
  layout: "horizontal"
}
".taskText": {
  width: Ti.UI.FILL,
  height: Ti.UI.SIZE,
  textAlign: "left"
}
"#task": {
  font: {
    fontSize: "18sp",
    fontWeight: "bold"
  }
}
"#limitTime": {
  font: {
    fontSize: "14sp"
  }
}

タスク完了の確認画面

完了タスクは別に表示されている

終わりに

これで最低限の機能実装が終わりました。 Titanium Cloud Services と連動させたり、完了したタスクを復活させたりと、機能改善の余地は沢山残っています。もしも興味があれば改良してみてください。今回作成したアプリケーションは GitHub で公開しています。 MIT License です。