インフィニットループ 技術ブログ

2012年09月21日 (金)

著者 : take

Titanium Mobile で Android, iOS 両対応のアプリを開発する際の注意点

大学生活にも慣れてきました。 18 歳の夏を満喫している take です。こんにちは!
会社では、 JavaScript を書くお仕事をしています。楽しいです!
さて、 前回 は Titanium Mobile で Android アプリを書く際にハマるポイントをいくつか紹介しました。
最近、Titanium Mobile なのだから、1つのコードで Android, iOS 両対応するアプリを書こう!ということで、コードを書きながら、予め知っておくと開発が捗るポイントを何点か見つけましたので、ここで紹介したいと思います。

※これから紹介する方法 (コード) は私個人が勝手に考えて実践しているものなので、ベストプラクティスであると保証することはできません。間違っている点や、さらに良い方法があればコメント等お待ちしています。
方針は、 Android も iOS も共通のコードで同じ UI のアプリを作る事です。

Menu を実装する

Android と iOS の両対応でまずはじめに問題になるのは、iOS にはメニューを表示する機能が無い事です。
Android も ICS 以降では、基本的にメニューキーがありません。(古いアプリのためにメニューを表示する事は可能)
「メニューが無いなら実装すれば良いじゃない」
無いものは実装してしまいましょう。最近の Android のアプリには「≡」みたいなマークの付いたメニューが実装されているので、それを参考にします。

menu - ios

iOS

menu - android

Android

iOS ではアプリ側からアプリを終了することができないため、 Exit の項目を設置していません。

Android では、 Ti.UI.createWindow した Window を全て close() するとアプリが終了します。 iOS で同じ事を試すと、真っ白な画面が表示されるだけでアプリは終了されません。
[追記] Menu と同様に、 Back ボタンも iOS にはありませんので、実装する必要があります。
方法としては、今表示している Window を close するだけです。 (iOS のみ、最後の Window は close しないように注意してください)

シングルコンテキスト

Mobile Best Practices にも書かれている通り、公式でシングルコンテキストを用いることが推奨されています。
シングルコンテキストで書く利点は、

  • パフォーマンスが良い
  • JavaScript らしい書き方ができる
  • CommonJS モジュールが活用できる (Ti.include は使わない)

などがあるそうです。

var win = Ti.UI.createWindow({url: "source.js"});
Ti.include("source.js");

という書き方はもう古いです。
これからは

var obj = require("view");

と書きましょう。
CommonJS の require を使う利点は、

  • コードが分離される
  • 変数スコープが変わるので変数名が衝突しない
  • オブジェクト指向ができる

注意点ですが、 require するときのパスは、どこから呼び出す場合でも Resources ディレクトリからの相対パスで指定します。(Android, iOS 共にこの方法で動きます)
Resources ディレクトリ直下に置くファイルは app.js のみにして、他のコードは、android, iphone, ui, lib に分けます。

View 毎にコードを分離する

先程の view.js に書かれるのはこのようなコードです

module.exports = function () {
  var view = Ti.UI.createView();
  var label = Ti.UI.createLabel({
    text: "Hello"
  });
  view.add(label);
  return view;
};

view.js を呼び出すコード

var win = Ti.UI.createWindow();
var view = require("view")();
win.add(view);
win.open();

こうすることで、View 毎に コードを分離する事ができます。
View を分離するときは、画面遷移を考えると簡単に分離する事ができます。
(1つの Window には、1つの View しか add しないと考えると分かりやすくなります)
View という分かりやすい単位毎にコードを分離することで、コードの見通しが良くなりメンテナンスがしやすくなります。
ここで、分離された View 毎に同じデータを共有する問題が出てきます。
View を require するときに、変数として g (グローバルオブジェクト) と 呼び出す View で必要になる引数を与えます。
グローバルオブジェクトは app.js で定義し、 View を呼び出す際に必ず 第一引数に渡すことで共有します。

Menu と Window はそれぞれ分離する

Menu を実装する、 View 毎に分離すると言いましたが、 Menu と Window のコードはどこに書くでしょうか。
Menu は1つのかたまりで CreateMenu.js として分離し、Window は生成部分だけ抜き出してまとめ、 CreateWindow.js として分離します。
CreateWindow.js に全ての Window が集まり、見通しが良くなります。

複数の View から利用するロジックは UI から分離する

View 毎に分離したコードの中に ロジックを書く事になりますが、その View 以外でも同じ処理をする場合は UI から分離します。
Resources/lib 辺りに入れて、必要な View から require しましょう。

View 等のサイズ指定は dip で行う

Android でマルチディスプレイ対応する場合の問題ですが、 android:anyDensity=”false” に設定して、 “dip” という単位でサイズを指定します。
anyDensity=”true” で数値を直接指定した場合と同じサイズになりますが、 anyDensity=”true” にすると Titanium のモジュールを使う場合等に問題になることがあります。
グローバルオブジェクトに

g.dip = function (size) {return String(size + "dip")};

を追加して、 g.dip(20) のように指定する事をお勧めします。
すると、万が一 px 指定に戻したくなった場合でも、 “dip” を “px” に直すだけの書き換えで済むため、かなり手間が省けます。

iOS で動くが Android で動かない コードや UI に注意する (逆もあるかも)

UI では、 TableView で TableViewRow に View を重ねた場合に、 View をタップしたときの click event が TableViewRow に伝播しないという問題があります。
コードでは、

String.prototype.hoge = function () {return this + "hoge"};

を app.js で定義したとして、require される MainView.js で “hoge”.hoge すると、
Uncaught TypeError: Object hoge has no method ‘hoge’
と言われてしまいます。
このどちらの例も、 iOS では動きます。
要点をまとめると以下の通りです。

  • Menu を実装する
  • シングルコンテキストで書く
  • require するパスは Resources からの相対パス (.js は不要)
  • グローバルオブジェクトを定義して全ての View に引数として渡す
  • 画面遷移を意識して View 毎に分離する (1Window 1View)
  • Menu と Window はそれぞれ分離する
  • 複数の View から利用するロジックは UI から分離する
  • サイズ指定は dip で行う
  • iOS で動くが Android で動かないコード, UI に注意する

最後まで読んでいただきありがとうございました。

ブログ記事検索

このブログについて

このブログは、札幌市・仙台市の「株式会社インフィニットループ」が運営する技術ブログです。 お仕事で使えるITネタを社員たちが発信します!