2012年7月19日木曜日

iPadでは大きな画像のリサイズが遅い

やることが多くて忙しかったので、久しぶりの投稿です。コンテンツ待ちで保留中の、Titanium Mobileを使ったiPadアプリですが、少しずつ動き出しました。新しいiPadを入手して、レチナ・ディスプレイでもテストしたら、思いがけない発見がありました。

 

レチナではない普通のディスプレイでは普通に動いていたアプリですが、レチナ・ディスプレイで動かしたら、処理が極端に遅い部分を発見しました。普通のディスプレイだと1秒ぐらいで終わる処理が、数秒間も余計に待たされるのです。あきらかに遅く、このままでは公開できないほどでした。

遅い原因が分からないので、どの部分が遅いのか、まずは調べることにしました。メッセージを表示するためのラベルを追加して、処理の区切りごとに英字を表示させます。こうすると、どの部分で遅いのか簡単に特定できますから。実際に試してみると、予想もしない部分で遅くなっていました。この投稿のタイトルに書いたとおり、大きな画像のリサイズです。

 

このアプリは、コンテンツである画像の表示がメインとなります。用意した画像を画面いっぱいに表示したり、レイアウトの一部として画像を含め、縮小して表示したりします。画像の大きさは、普通のディスプレイ用の1024×768ピクセルと、レチナ・ディスプレイ対応版の2048×1536ピクセルの両方を用意しています。そのためレチナ・ディスプレイでは、2048×1536ピクセルの画像をリサイズして表示することになります。GPUを使わずCPUで処理しているためでしょうか、明らかに時間がかかっています。

本当にリサイズが重くて遅くなっているのか、リサイズした画像を追加して、リサイズ処理をなくしてみました。すると予想どおり、数秒間の待ちが完全に消えました。やはり大きな画像のリサイズが重かったのでした。パソコン用のCPUではなく、携帯電話用のCPUですから負荷が大きいのでしょう。

 

レチナ・ディスプレイに対応するため画像サイズが大きくなり、さらに画像の数が多いので、できるだけ容量を増やさないようにと考慮し、画像をリサイズして使う設計にしました。しかし、そんな方法が裏目に出てしまいました。処理が遅くないように作らないとイライラして使えません。容量が増えてしまいますが、リサイズした画像を追加で全部用意することにしました。

パソコンと違って、まだまだ考慮すべき点があるのですね。勉強になりました。

2012年5月7日月曜日

SDK 2.0.1でも描画問題への対処は必要(3)

Titanium SDK 2.0.1GA2で、描画関係のバグが解消されているか調べる話の続きです。iPhone用アプリの開発で経験した、画面を回転させたとき、ImageViewやLabelが変な位置に描画される問題を取り上げます。前の投稿で直っていると書きましたが、対処方法を実施しても消えなかったバグが出なくなっただけで、対処方法を不要するレベルで直っているかは不明でした。そこを調べたので報告します。

 

まずはバグの内容を。画面を回転させたときにアニメーションで表示されますが、縦横表示で同じViewを使い、View上のUI部品の位置を回転時に変更すると、設定したtopやleftの値とは全然違う位置に表示される問題です。発生する条件ですが、複数のImageViewの位置を変更させると起こるようです。単に位置が変になるだけではなく、本来なら下に隠れているImgeViewの一部も表示されてしまいます。対処方法は、次のような形でした。見えているUI部品だけの位置を変更し、下に隠れているUI部品はsetTimeoutで遅延させて変更する方法です。この対処方法でほとんど解消したのですが、たまに実機でのみ、一部のLabelが変な位置に表示されます。

以上にような状態のまま2.0.1GA2で再ビルドすると、実機でも変な位置の表示が解消されました。その点では、問題が解消されたといえます。しかし、本来なら特別な対処方法(一部の設定を遅延させる)を使わなくても、プロパティで指定した位置に表示すべきものです。それが直っているかどうか、シミュレータと実機の両方で確認してみました。

 

まず、対処方法を加えたコードです。画面が回転したときに呼び出される関数として作ってあります。表示中のUI部品だけは位置プロパティを変更し、残りのプロパティ変更は遅延した別関数として作りました。このような形で作ると、シミュレータ上ではバグが完全に消えました。ただし実機でのみ、少しバグが出ます。

// 1つだけImageViewを変更する
function changeOrientF() {
    var orient = Ti.Gesture.orientation;
    if (orient == Ti.UI.PORTRAIT || orient == Ti.UI.UPSIDE_PORTRAIT) {
        imgView1.height = 240;
        imgView1.width = 320;
        ...
    } else if (orient == Ti.UI.LANDSCAPE_LEFT || orient == Ti.UI.LANDSCAPE_RIGHT) {
        imgView1.height = 320;
        imgView1.width = 427;
        ...
    }
    setTimeout(changeOrient2F, 200); // 0.2秒後に動かす
}
// 残りのImageViewを、時間差を付けて変更する
function changeOrient2F() {
    var orient = Ti.Gesture.orientation;
    if (orient == Ti.UI.PORTRAIT || orient == Ti.UI.UPSIDE_PORTRAIT) {
        imgView2.height = 240;
        imgView2.width = 320;
        ...
    } else if (orient == Ti.UI.LANDSCAPE_LEFT || orient == Ti.UI.LANDSCAPE_RIGHT) {
        imgView2.height = 320;
        imgView2.width = 427;
        ...
    }
} 

このコードを、本来の形に戻します。遅延する関数として2つに分けるのではなく、すべてのプロパティ変更を一緒にして、1つの関数として作ります。具体的なコードは、次のようになります。

// すべての変更を1つにまとめる
function changeOrientF() {
    var orient = Ti.Gesture.orientation;
    if (orient == Ti.UI.PORTRAIT || orient == Ti.UI.UPSIDE_PORTRAIT) {
        imgView1.height = 240;
        imgView1.width = 320;
        ...
        imgView2.height = 240;
        imgView2.width = 320;
        ...
    } else if (orient == Ti.UI.LANDSCAPE_LEFT || orient == Ti.UI.LANDSCAPE_RIGHT) {
        imgView1.height = 320;
        imgView1.width = 427;
        ...
        imgView2.height = 320;
        imgView2.width = 427;
        ...
    }
}

ご覧のように、難しい変更ではありません。バグが解消していれば、一部のプロパティ変更を遅延させなくても、正常な位置に表示されるはずです。

 

さて実際に動かした結果ですが、シミュレータでも実機でも、変な位置に表示されるバグは出ませんでした。ImageViewもLabelも、プロパティで設定した位置に表示されます。動作中のいろいろなタイミングでiPhoneを回転しましたが、途中の状態はさておいて、最後には正常な位置で表示しました。アプリのアニメーション中に回転アニメーションが加わっても、いつも正常な位置に収まります。2.0.1GA2では、バグが解消されているようです。

画面回転での表示バグは、いろいろな対処方法を試しましたが、どうしても解決しなかったものでした。SDKのバージョンアップで解消され、本当に良かったです。バグが消えたことで、特別な対処方法を用いる必要がなくなりました。

 

ここまで3回の投稿を整理すると、2.0.1GA2で対処が必要な描画問題は、フラッシュバック症状だけになりました。これは前から対処方法を見付けていますから、ぜんぜん大丈夫です。2.0.1GA2では対処方法をpostlayoutイベント処理で実現しますが、非常に簡単な変更でした。この1つだけで大丈夫になったということは、Titanium SDKのレベルアップではないでしょうか。まさに意味のあるバージョンアップですね。

2.0.1が安定したバージョンになれば、もう安心して公開できます。画像がメインのアプリなので、あとは画像とテキストの制作待ちですが、SDKの安定バージョンが出る頃には、制作も終わっているでしょう。めでたし、めでたし。

2012年5月3日木曜日

SDK 2.0.1でも描画問題への対処は必要(2)

Titanium SDK 2.0.1GA2を使って、描画関係の問題が解消しているか調べる話の続きです。その問題とは、hide中のViewで、ViewにaddしたUI部品の位置や内容を変更してから、Viewをshowしたとき、変更前の状態が一瞬表示されることです。フラッシュバック症状と呼んでいます。前回は、UI部品の変更を箇所を何も変えず、setTimeoutで処理していた箇所を、postlayoutイベント処理に変更した話でした。この簡単な変更だけでも、問題なく動きました。

 

いよいよ今回は、UI部品を変更する箇所の改良です。Titanium SDK 2.0.1に追加された、UI部品の複数プロパティ変更を、1つの変更のように扱う処理を利用します。単独で使うupdateLayout関数と、ペアで使うstartLayout関数とfinishLayout関数の、2種類が用意されています。どちらも、ViewまたはUI部品で使う関数なので、単独のViewまたは単独のUI部品が対象となります。

今回のアプリでは、View上の複数部品を一緒に変更するため、すべての変更が終わってから、変更が完了したと知らせる必要があります。ペアで使う関数しか役に立ちません。実際のコードは次のようにしました。

// 変更の開始を知らせる
imgView.startLayout();
label1.startLayout();
label2.startLayout();
view.startLayout();

// 各UI部品を変更
imgView.top = 120;
imgView.left = 200;
imgView.image = photoName[i];
...

// 変更の終了を知らせる
imgView.finishLayout();
label1.finishLayout();
label2.finishLayout();
view.finishLayout);

// viewを表示
view.show();

シミュレータ上で動かしてみると、まったく変わりません。変更前から正常に動いているため、当然でしょう。フラッシュバック症状が出るかどうか確認するために、postlayoutイベント処理をsetTimeout処理に戻し、遅延時間を1ミリ秒に設定して動かしました。しっかりとフラッシュバック症状が出ます。症状を消すためには、はやりpostlayoutイベント処理が必要でした。

この後、viewをshowする処理の位置などを変更しながら動きを観察していて、大事なことに気付きました。Viewをhideしている状態ですから、描画機能は動いてません。その状態でView上のUI部品を変更しても、プロパティの値が変更されるだけです。そしてViewがshowされたときに、設定されたプロパティで描画内容を生成します。そのshowの最初に、フラッシュバック症状が発生するというわけです。変更前のフラッシュバック症状がshowしたときに出るということこそ、showした時点から描き始めている証拠です。

整理すると、startLayout関数とfinishLayout関数は、showされてる状態で有効なのであって、hideされているときに使っても意味がないのです。もちろん、ここで試さなかったupdateLayout関数も同様です。今回のアプリのようにhide中の変更では、単に無駄に処理を加えているだけとなります。意味なしです。というわけで、startLayout関数とfinishLayout関数を削除しました。

 

新しい関数の使用条件を理解していなかったので、予想外の結果となりました。結局、フラッシュバック症状への対応は、前回の投稿と同じまま、setTimeout処理をpostlayoutイベント処理に変更するだけで完了です。回り道をしましたが、startLayout関数とfinishLayout関数などの役割を理解できたので、良しとしましょう。この結果を知っていたら、前の投稿と一緒に書いて構わなかったですね。まあ、こんなこともあります。

2012年5月1日火曜日

SDK 2.0.1でも描画問題への対処は必要(1)

このブログでは、Titanium Mobileを使った際の画面表示の問題と格闘してきました。少し前の投稿では、リリースされたSDK 2.0.1GA2を使うことで、最大の問題だったImageViewやLabelが変な位置に表示される症状が、直ったとの速報を書きました。あれから使い続けていますが、症状は1回も発生していません。直っているのは確実なようです。

描画関係では他にも問題があり、その1つ1つが直っているのか、直っていないなら同じ対処方法で大丈夫なのか、1つずつ見直したいと思います。まずは、Viewのレイアウト変更への対処を取り上げます。

 

以前の投稿「Viewはshowしたときに描き直されるもの?」で、レイアウトを変更したときの対処方法を書きました。おさらいすると、問題は、hideしていたView上にあるImageViewやLabelの画像やテキストや位置を変えた後、showすると変更前の状態が一瞬だけ表示されることでした。フラッシュバック症状と呼んでいます。対処方法は、Viewの透明度を限りなく透明に設定してからshowし、setTimeoutで遅れて透明度を不透明に戻す方法でした。

まず調べたのは、SDK 2.0.1GA2でも、UI部品を変更する前の状態が一瞬表示されるフラッシュバック症状が出るかどうかです。setTimeoutの遅延時間を極端に短い1に設定して、シミュレータ上の動作を見てみました。結果は、前と同じです。SDK 2.0.1GA2でも、同様のフラッシュバック症状が出ました。つまり、この部分の動きは変わっていないということです。

ただし、何も対処していないわけではありません。SDK 2.0からは、プログラムの作り方で対応するように、新しい機能が追加されています。the UI Layout Systemが更新され、UI部品の大きさに関するデフォルト値が変わりました。同時に、描画での作業終了を考慮した機能が追加されています。それぞれのUI部品ごとに、複数プロパティを変更するときの処理を1つとして扱い、全部が終ったら描画する形も可能になりました。具体的な方法が2つ用意されていて、まずupdateLayout関数では、複数のプロパティを一度に指定できます。もう1つのstartLayout関数とfinishLayout関数はペアで使い、この間にプロパティの変更処理を入れます。

さらに、描画内容を生成し終わるまでの待つ機能が加わっています。それがpostlayoutイベントで、変更するViewやUI部品にイベント処理を加えれば、setTimeoutで処理を遅らせる必要はありません。setTimeoutで遅らせる方法では、少し余裕を持った待ち時間を設定するため、全体として処理が遅くなります。ところがpostlayoutイベントで知らせる方法だと、描画内容を生成し終わったら始められますから、無駄な待ち時間は生じないはずです。

SDK 2.0.1での変更点は、Appceleratorの開発者向けドキュメントに記述してあります。興味のある方は「Transitioning to the New UI Layout System」を読んでみてください。

 

開発中のアプリで、SDK 2.0.1GA2での改良を反映させてみました。フラッシュバック症状は前と同様に出ますから、時間を遅らせて表示させる処理は必要のままです。ただし、実現方法としては、setTimeoutで遅延させる方式から、postlayoutイベントで処理を開始する方式へと切り替えます。これで無駄な待ちが少しは減るでしょう。まずは、これまで実施していたsetTimeoutによるコードです。

// 修正前(setTimeoutを使用)
view1.hide();          // view1を非表示にします
view1.opacity = 0.001; // view1を限りなく透明にします
label1.top = 40;       // view1上のUI部品のプロパティを変更して、画面上のレイアウトを変えます
label1.left = 30;
imgView.top = 120;
...
view1.show();                  // view1を再表示します
setTimeout(resetOpacityF, 50); // 50ms後に、不透明に戻すfunctionを起動させます
// この関数は、ここで終了

function resetOpacityF(){      // 時間差攻撃で、view1を不透明に戻します
    view1.opacity = 1;
}

これを、処理内容は同じまま、postlayoutイベントで処理するコードに切り替えます。具体的には、次のように作ります。

// 修正後(postlayoutイベントを使用)
view1.hide();          // view1を非表示にします
view1.opacity = 0.001; // view1を限りなく透明にします
label1.top = 40;       // view1上のUI部品のプロパティを変更して、画面上のレイアウトを変えます
label1.left = 30;
imgView.top = 120;
...
view1.show();                  // view1を再表示します
view1.addEventListener('postlayout', resetOpacityF); // イベント処理を設定します
// この関数は、ここで終了

function resetOpacityF(){      // postlayoutイベントで、view1を不透明に戻します
    view1.removeEventListener('postlayout', resetOpacityF); // イベント処理をクリアします
    view1.opacity = 1;
} 

setTimeoutの代わりとして、postlayoutイベント処理関数をaddEventListenerでViewに加えています。これで描画内容の生成終了待ちとなります。postlayoutイベントが発生すると、設定したイベント処理関数の実行が始まり、まず最初にremoveEventListenerでイベント処理関数を削除し、本来の処理を開始します。以上のように、ほとんど前と同じままで、変更が完了してしまいました。

 

view1をshowした直後にpostlayoutイベント処理を追加し、そのまま待ちます。本来ならshowする前にイベント処理を追加すべきなのですが、描くのに時間がかかるためでしょう、これでも問題なく動きました。さらには、UI部品の変更を1つにまとめる変更もしなければならないのですが、それをする前に試しに動かしたら、正常に動いてしまいました。すぐにremoveEventListenerを実行しているためでしょうね。アプリの動作としては、フラッシュバック症状が発生せず、とくに副作用もありません。こんなに簡単に動いて良いのでしょうか、と疑問に思うぐらい簡単に動きました。

本来であれば、UI部品の箇所も一緒に変更して公開すべきでしょう。でも、簡単な変更でも正常に動いたので、これも面白い情報だと思って公開しました。完全ではない変更でどのように動くかも、意外に貴重な情報となるからです。期待どおりに動かなかったケースで、こういう完全でない変更での動きが、解決方法を見付けるヒントになったりしますので。

 

動いたのを確認しただけでは、ちょっと満足感が不足です。postlayoutイベントが発生するまでの時間はどの程度なのか、やはり気になりますよね。そこで、イベント発生までの時間を計測してみました。addEventListenerの直前に時刻を計り、removeEventListenerの直後にも時刻を計って、差を求めるだけです。イベント処理の追加と削除を含めたのは、これらの処理も含めた経過時間を知りたかったからです。計算した時間差を表示する機能を加えて、実際に実行してみました。

描画内容を生成する時間は、UI部品の種類や数や変更内容によって左右されます。計測結果は、あくまで今回のアプリの場合です。postlayoutイベントを使った箇所は2つで、両方とも測定しました。1番目の箇所は、変更するUI部品の数が6つで、4つがLabel、2つがImageViewです。6つとも位置を変更し、それぞれの値であるテキストと画像も毎回変更します。たまにですが、一部のUI部品で位置だけ変更しない場合もあります。こうした条件のアプリをシミュレータ上で計測したところ、最低では0、最高で41の値となりました。数値の単位はミリ秒で、マシンは現行の13インチMacBook Air(Core i5 1.7GHz Dual)です。発生頻度が一番高いのは0で、全体の3割ぐらいを占めていました。0を含めた一桁台が全体の半分程度ありました。数値が極端に大きいときは、画像をメモリーに読み込んでいるとか、ガベージコレクタが動いているとか、特別な条件なのでしょうか。原因は不明です。

2番目の箇所は、変更するUI部品が半分の3つで、2つがLabel、1つがImageViewです。これらへの変更内容は1番目と同じですが、透明度を変更する処理が加わっています。上記のサンプル・コードは、この2番目の箇所のものでした。計測すると、50〜52と値はほぼ一定でした。このようにバラツキがほとんどないのが普通だと思います。1番目の箇所でバラツキが生じた理由が分かりません。

同じ計測を、実機でも試してみました。初代iPadで実行すると、1箇所目は最低が6で、最高が62でした。全体的に値が大きくなっています。頻度としては小さな値の比率が大きく、とくに一桁が半分程度を占めるという変な結果となりました。バラツキの傾向も非常に似ていて、シミュレータが正常に機能していることを証明した感じです。まあ、当たり前の結果でしょう。2番目の箇所もシミュレータと似ていて、値は56〜67とバラツキは小さいです。1番目の箇所と同様に、シミュレータよりも少し遅くなっています。

余談ですが、setTimeoutでは50に設定していたので、最高が67という結果では、修正前の50という値が小さすぎたのかも知れません。しかし、実機でかなり使いましたが、描画の問題は出ませんでした。postlayoutイベントが発生するタイミングに多少の余裕があるのか、描画内容としてギリギリだと目立たないのか、その辺は分かりません。修正前の50という値は、単なる偶然ですが、一番遅い初代iPadによる実機での動作としては絶妙な値だったのでしょう。

 

計測結果では、片方の描画は非常に短い時間が多くなりましたが、実機を触っている限り体感できません。もともと短い時間なので、少しぐらい減っても体感できないのでしょう。今回の修正により、固定した時間だけ遅らせるのではなく、実際の描画内容生成が終わるまで待つ形になりました。処理としては、より良い形になっています。あまりにも簡単な修正でしたが、最適化された形となりました。今後も、似たような状況では、postlayoutイベント処理を使うでしょう。

UI部品の変更は、次の投稿で書きます。また別な症状の確認も残っていますから、それは後でということで。

2012年4月26日木曜日

間接参照は利用側の目的指向で作成する

じっくりとテストする時間が取れないので、今日は少し、ソフトウェアを作るときのコツというか考え方について少し書きましょう。取り上げるのは、基本中の基本といえる間接参照です。

 

ソフトウェアの中で、幅広く利用されている作り方に間接参照があります。データ自体を直接参照する代わりに、間に1つ以上の参照を挿入し、何段階かの参照を組み合わせる方法です。ハードウェアに近い部分からアプリケーションまで、幅広く使われています。

たとえば、OSのメモリー管理を間接参照にすることで、使っているメモリー部分を集めるために、メモリー上のデータをOSが移動しても、アプリでは問題なくメモリー上のデータが使えるのは、間接参照のおかげです。オブジェクト指向の仕組みも、間接参照の凝った使い方です。ソフトウェアの柔軟性を増すための基礎的な仕組みが間接参照なのです。

もちろん、欠点もあります。参照回数が増えることで、そのための処理が必要となり、処理速度は低下します。しかし、CPUパワーが向上した現在では、あまり気にならなくなりました。それ以上に、ソフトウェア変更での柔軟性を増す価値のほうが格段に大きくなっています。

 

このブログの最初のほうで、UI部品を生成するための関数を紹介しました。最終版は次のようなコードでした。

// ボタンの生成(base.js)
exports.createBtnF = function(_title, _fontSize, _height, _width, _top, _left){
    return Ti.UI.createButton({
        title:_title,
        font:{fontSize:_fontSize},
        height:_height,
        width:_width,
        top:_top,
        left:_left
    });
}
// 上記の関数を利用する(main.js)
bbb = require("base"); // レベル1を使えるようにする
(function() {
    bb.win = bbb.createWinF('prod_edit');
    var btnOpenPe = bbb.createBtnF(bb.win, '商品編集', 24, 40, 200, 100, 32);
    btnOpenPe.addEventListener('click', openPeWinF);
    ...
})();

これも間接参照です。UI部品の生成コードを直接書く代わりに、生成関数を呼び出す形にします。メリットとしては、生成する部分のコードが1行で済むことです。加えて、生成するボタンのデフォルト設定を自由に変えられます。また、デフォルトを変更して使うボタンでは、生成直後に変更する部分(主にプロパティ)の変更コードを追加するので、どこを変えたのか一目瞭然となります。間接参照ならではのメリットです。

このような使い方を、間接参照の利用方法として捉えると、設定の挿入と見ることができます。参照を間に1つ挿入しながら、挿入した部分でデフォルト設定を持てるようにしてるわけです。デフォルト設定を持つことで、デフォルト設定を変更するときの柔軟性を確保するとともに、利用する側での記述も減らしています。

 

他の例では、プログラム内で使う定数宣言も間接参照です。C言語の時代から普通に行なわれていました。JavaScriptにも同様の構文があり、constを付けて宣言します。JavaScriptに限りませんが、定数宣言の作り方には少し注意点があります。例で説明したほうが分かりやすいでしょうね。アプリで表示するメッセージの色を定義する場合を考えてみましょう。エラーなら赤、注意なら青、処理が成功した報告では緑、その他は黒と決めました。そのまま、次のように作ることも可能です。

// メッセージの色を定数宣言(色で名前を付ける)
const COLOR_RED = '#f00';    //色:赤
const COLOR_BLUE = '#00f';   //色:青
const COLOR_GREEN = '#0a0';  //色:緑
const COLOR_BLACK = '#000';  //色:黒

このように作れば、色を細かく変える場合に変更は一箇所で済みます。しかし、処理成功を青、注意をオレンジに変更したくなったらどうでしょう。もちろんCOLOR_GREENを青色の00fに変更すれば可能ですが、GREENと定義してあるのに色が青では混乱の原因になります。それを避けようとして、COLOR_GREENと記述してある箇所を、すべてCOLOR_BLUEに置き換えることになるでしょう。

同じ内容を、もっと違った視点で作ると、より変更に強い作り方になります。色の名前で定義するのではなく、各色の利用目的の名前で定義するのです。具体的には、次のように作ります。

// メッセージの色を定数宣言(色の利用目的で名前を付ける)
const COLOR_ERROR = '#f00';  //色:赤
const COLOR_ALERT = '#00f';  //色:青
const COLOR_OK = '#0a0';     //色:緑
const COLOR_STD = '#000';    //色:黒

色の利用目的で名前を作り、何色なのかはコメントで示しています。このような形だと、宣言した名前が「エラーの色」となっているため、エラーの色は何色でも大丈夫になります。エラーの色は、エラー表示の箇所でしか使いませんから、余計な置き換えも生じません。色の変更も、色の値とコメントだけで済みます。毎回、たった1行の変更で済むでしょう。

 

実は、上の例こそ、間接参照を上手に使う鍵なのです。間接参照を作るときに間に入れるもの(上の例ではconstによる定数宣言)は、それを「使う側の利用目的ごとに別々に用意する」のが基本なのです。逆に、色の名前で作ったものは、利用目的ではなく、値そのものの名前で作ったと捉えることができます。値そのもので名前作ったわけですから、値の変更に名前が影響されます。その名前は参照に使う名前なので、利用する側の変更も生じさせ、変更への柔軟性が低下するわけです。間接参照を名前で参照する以上、「参照で使う名前を変えなくて済むように作る」というのが大事なのです。

「別々に用意する」の意味が分かりやすいように、同じ色定義の例を利用しながら、もう少し別な状況を考えてみましょう。メッセージの色に加えて、枠線の色も何種類か使う状況だとします。この場合も、色の名前で定数宣言すると、前にも増して大変なことになります。メッセージの赤と枠線の赤が一緒に宣言してありますから、片方だけ変えたいときに、色の値を変更すると、もう片方の色も一緒に変わってしまいます。両者を区別するためには、二つの宣言に途中で分けなければなりません。具体的な作業としては、COLOR_REDを使っている箇所を全部調べて、片方を別の名前に変更する必要があります。考えただけでも大変そうです。直し忘れが生じていて、公開してから気付いたなんて状況も起こりそうで怖いです。

では、どのように作るべきなのでしょうか。鍵は「使う側の利用目的ごとに別々に用意する」ですから、メッセージの色と枠線の色では利用目的が異なります。たとえ同じ色であっても、別々に定義するのが基本です。次のような形になるでしょう。

// メッセージの色を定数宣言
const COLOR_MSG_ERROR = '#f00';  //色:赤
const COLOR_MSG_ALERT = '#00f';  //色:青
const COLOR_MSG_OK = '#0a0';     //色:緑
const COLOR_MSG_STD = '#000';    //色:黒
// 枠線の色を定数宣言
const COLOR_BORDER_EDIT = '#f00';   //色:赤
const COLOR_BORDER_BROWSE = '#00f'; //色:青
const COLOR_BORDER_CHECK = '#0a0';  //色:緑
const COLOR_BORDER_ETC = '#000';    //色:黒

色の定数宣言だと示す言葉COLORに続いて、利用目的の分類または対象となる言葉のMSGかBORDERを付けます。最後に、分類または対象ごとの、具体的な目的を示す言葉を加えれば完成です。今回の例では、最初の定数宣言をMSG入りに変更しました。同じことが、開発の途中で起こりえます。そうなると利用する側の変更が生じるでしょう。ですから、メッセージの色しか作らない場合でも、最初からMSGを入れて定数宣言するのが、より良い作り方といえます。

上記の定数宣言は、まったく同じ色を宣言していますが、利用する目的が異なるから別々に作ったわけです。定数宣言の重複だと考える必要はありません。この作り方のほうが、変更への柔軟性は格段に上です。

 

ここまででも、予想外に長くなってしまいました。続きは、気が向いたらというか、別な機会に書くということで。

2012年4月23日月曜日

Titanium StudioでのXcode選択を4.2から4.3へ変更

前回の投稿で、Titanium Studioを2.0.1にアップデートし、Xcode 4.3.2を使ってテストした話を書きました。その際に少しだけ面倒な手順を踏んだので、その話を書きましょう。

 

アップデートの順番が関係しているため、行なった順に説明します。最初はTitanium Mobile SDK 2.0.1.GAの通知が来て、それをインストールしました。続いて同GA2の通知が来て、それもインストールしました。この時点で、2.0.1はまったく使っていません。そのまま1.8.2を使い続けていると、Titanium Studio 2.0.1のアップデート通知が届きました。すぐにインストールして、少し使ってみます。問題なく使えました。

Titanium Studio 2.0.1は、Xcode 4.3に対応していたことを思い出し、Xcode 4.3をインストールすることにしました。アップルの開発者向けサイトにアクセスすると、最新版のXcode 4.3.2は、Mac App Storeからダウンロードする形になっていました。App Storeアプリを使ってアクセスしたら、ダウンロードのページが開きます。使用者の感想が書いてあって、Xcode 4.3からはボリューム直下の「Developer」フォルダがなくなり、アプリ内に移動したことへの文句などがありました。おかげで、そのことを知ることができました。

余談を少しだけ。文句を言ってる人もいましたが、「Developer」フォルダをアプリ内に持つことは良い改良だと思います。ボリューム直下の「Developer」フォルダだと、マシン上には、Xcodeアプリを1つだけしかインストールできません。インストールはできるのですが、細かな互換性の問題などで1つしか正常に動かないでしょう。反対にアプリ内に持つ形だと、バージョンの異なるXcodeアプリを何個でも併用できます。「Developer」フォルダが別々な場所にあることで、互いに影響を受けないからです。もちろん、別々のフォルダになっているため、追加するファイルを両方に入れなければならない欠点も生まれます。しかし、その欠点も、入れる入れないを別々に決められる利点と背中合わせなので、一概に欠点とは言えないでしょう。Xcodeに付属するアプリに関しても、別々に持つ形になりますが、これもバージョンの異なるアプリを混在できるというか、それぞれ別々に持てる点でメリットが大きいと言えます。

話を戻しましょう。Xcode 4.3.2のインストールです。Mac App Storeからダウンロードを選ぶと、ダウンロードからインストールまで自動で行なわれます。気付いたときには、「アプリケーション」フォルダ内にXcode 4.3.2がインストールされていました。

 

さて、ここからが本題です。Titanium Studio 2.0.1を先にインストールしたためか、Titanium Studioからシミュレータなどを起動させると、古いXcode 4.2が使われてしまいます。まあ当然の結果でしょう。試しに、古いXcode 4.2が入っている「Developer」フォルダをゴミ箱に入れてからTitanium Studioを動かしても、エラーが出るだけで、Xcode 4.3.2に切り替わってはくれません。これは少し残念だけど、当然でしょうね。

ビルドしたアプリを、とりあえずXcode 4.3.2動かそうと思い、古いXcode 4.2の「Developer」フォルダに入っているTitanium Mobile関係のファイルを、Xcode 4.3.2側にコピーしてみました。Xcode 4.3.2側というのは、Xcode 4.3.2アプリの中に含まれている同様の位置関係のフォルダです。その後、ビルドで生成されたアプリのXcodeプロジェクトファイルをXcode 4.3.2で開き、再ビルドしてシミュレータを起動できました。

 

こんな形で動かすのはダメですから、普通にXcode 4.3.2が使えるように設定しなければなりませんね。Titanium Studioの環境設定を探すと、それらしい設定がありました。環境設定で「Aptana Studio」の「Titanium」を開いて、上から2番目に「iOS SDK Home」の設定があります。設定中のパスの文字列から、ここが指定箇所だと判断できました。古いXcode 4.2のフォルダを指しています。ここをXcode 4.3.2のフォルダに変更すれば、Xcode 4.3.2のシミュレータなどが普通に呼び出されるはずです。

この部分の設定方法が少し変わっていて、普通には設定できません。xcode-selectコマンドを打つようにとの記述があります。「More Details」のリンクがあるので表示させると、詳しい説明ページがウェブブラウザで開きました。説明どおりに実行するために、ターミナルを動かします。最初はTitanium Studioに含まれているターミナルのウィンドウを使おうとしましたが、コピーした文字列をペーストできません。仕方がないので、Mac OS Xのターミナルを起動し、xcode-selectコマンドとオプション、Xcode 4.3.2のパスをペーストして実行しました。最後に、Titanium Studioの環境設定に戻り、「iOS SDK Home」設定の「Refresh」ボタンをクリックして切り替えが完了です。正しく切り替えられたか確認するために、プロジェクトをクリアーしてからビルドして、シミュレータを動かしました。Xcode 4.3.2に付属のシミュレータ5.1が起動して、正常動作が確認できました。ちなみにシミュレータ5.1は、新しいiPadのRetinaディスプレイに対応しています。

おそらくターミナルで実行したコマンドか「Refresh」ボタンが、必要なファイルをコピーしてくれるのでしょう。でも私の場合は、先に手作業でコピーしてしまったため、コピーしてくれるか確認ができませんでした。正しい方法で行なわないと、正常な動きを知ることができないという駄目の典型ですね。消して再実行すれば確認できないことはないのですが、面倒なので行ないません。興味のある方は試してみてください。

Xcode 4.3.2を先にインストールしてから、。Titanium Studioを2.0.1にアップデートすると、Xcode 4.3.2に切り替えてくれるのでしょうか。少し興味がありますけど、古いTitanium Studioを再インストールしないと調べられないので試しません。これも、興味のある方は試してみてください。試すというより、Xcode 4.3.2を先にインストールしてから、Titanium Studioをアップデートしたほうが良いでしょうね。

今回はXcode 4.2から4.3へ変更しましたが、同じ操作方法で逆の変更も可能です。試しに変更してみても、簡単に戻せるから安心でしょう。ただし、切り替え方法が少し面倒なので、もう少し簡単になると嬉しいのですが。

 

以上のように、もうXcode 4.3.2に切り替えて使っています。まだXcode 4.2.1を残してありますが、4.3.2で問題なく使えると判断した時点で削除する予定です。

2012年4月22日日曜日

Titanium SDK 2.0.1でImageViewのバグが解消した様子

Titanium Studioも2.0.1にバージョンアップし、Titanium SDKも2.0.1.GA2のアップデートが来たので、ImageViewのバグがどのように変化しているのか、さっそく試してみました。ついでに、Xcodeも4.3.2を入れての組み合わせです。開発中のiPhoneアプリは、ソースコードなどを全く直さず、Titanium SDKとして、1.8.2を2.0.1.GA2に変更しただけです。

 

まずは、シミュレータでの動作確認。ここは前から問題なしだったので、そのまま通ると予想しました。確かに、動作としては問題なく表示されていました。ただし、コンソールにはワーニングがいくつも出ました。Titanium SDKが仕様変更されていて、直さなければならない箇所があるようです。あとで詳しく調べて、きっちり修正しましょう。

続いて、実機での確認です。ここもXcodeが4.3.2に変わっただけで、設定などは以前と同じです。Ad-Hocでアーカイブを作成し、iTunesでiPhoneに転送しました。ドキドキの動作確認ですが、まったく問題ありませんでした。今までだと、画面を何度も回転させると、ImageViewは何とか正常に表示されたのですが、一緒に表示するLabelの位置が変になっていました。以前はすぐに症状が出ましたが、かなり操作しても出ませんでした。どうやら直っているようです。

LabelもImageViewと同じ対策をすれば、前のSDKでも解決できたかも知れません。しかし、症状が出ている状態で、Titanium SDK 2.0.1.GA2での動作を確認したかったのです。直っていることを期待して、作成中のアプリで確かめたかったというわけです。

Titanium SDK 2.0では、描画関係に大きなメスが入れられたようで、根本的に改良されたのでしょう。それによって、一連のバグが直ったと思われます。今まで頑張って見付けた対策も、もしかしたら必要ないかも知れません。その辺も、あとで調べる必要があります。とりあえず、症状が消えたことで、安心しました。もっと本格的なテストは必要ですが、たぶん大丈夫でしょう。Titanium Mobileを避ける大きな理由が消えて、凄く嬉しいです。

 

Xcodeのみでの開発も、少しずつ勉強を進めています。Objective-Cでの開発も覚えておいて損はないでしょうから、このまま続けます。Titanium Mobileで作成中の3つめのアプリを、同じ形で作ろうと進行中です。まあ、保険という意味も少しはあります。もう必要なさそうですけどね。とりあえず、最初のWindowを表示しTableViewを動かす部分までは作りました。

Objective-Cを使って気付いたのは、Titanium Mobileでの作りやすさです。あらためて実感しました。まだObjective-Cに慣れてないという点もありますが、それを差し引いても、JavaScriptで気軽にさくさく作れる点は大きな魅力です。私自身、JavaScriptはブラウザ用に数行程度しか使ったことがなく、Titanium Mobileで本格的に勉強を始めました。ほとんど知識がなかったのですが、意外に早く作れるようになりました。やはりスクリプト言語の威力は凄いです。それに比べるとObjective-Cは、JavaやCを使ったときのような注意深さが必要です。

 

簡単なテストだけでしたが、Titanium SDK 2.0.1.GA2ではImageViewおよびアニメーション関連のバグが解消したようで、大きな懸念が取り払われました。2.0.1がどんどん使われ、安定した状態になるのを大いに期待してます。次のアプリも、Titanium Mobileで開発することになるでしょう。めでたし、めでたし。

2012年4月20日金曜日

Objective-Cも併用することに決めました

Titanium Mobileでアプリ開発を始め、さくさくと作れる点は非常に気に入っているのですが、バグとの格闘に疲れ気味です。また、最新iOSや純正の開発環境に追いつかず、少し前のバージョンで使う状況にも、少し残念な面があります。そこで、Objective-Cも勉強し、Xcodeのみでの開発もマスターすることにしました(ちなみに、Titanium Mobileによる開発でもXcodeが一部で使われてます)。

簡単にまとめると、Titanium Mobileでのアプリ開発と、Xcodeのみのアプリ開発を併用すると言うことです。使い分けの基準は、Titanium Mobileでバグに当りそうなときはXcodeのみ、それ以外はTitanium Mobileという感じでしょうか。同じ画面内でImageViewを複数使うようなアプリでは、Titanium Mobileを使わないようにします。

 

Xcodeのみの開発に必要が勉強は、昨日から始めました。まずObjective-Cの勉強で、友人から良い資料を教えてもらい、それを読み終わりました。約5時間ぐらいで、全体像が把握できました。記憶に頼ると忘れるので、特徴をメモしながらの勉強です。そのメモを見直すことで、いつでも素早く使えるようにしてます。Objective-Cは、もう大丈夫です。

続いて、アプリ開発の全体像と、フレームワークの理解です。簡単なアプリを作り始めていて、少しずつ理解していく感じでしょうか。少し悩んだのですが、Interface Builderを使わない開発を選択しました。現在進行中の、Titanium Mobileでのアプリ開発と似た形になりますね。2週間ぐらいで、だいたい把握できれば良いのですが。

 

Titanium Mobileでのアプリ開発の現状も、少しだけ。前の投稿で、画面回転したときに位置が変になるのは、ImageViewだけだと書きました。もっと違う条件でテストしていたら、Labelも位置が変になる症状を見付けました。もちろん、Labelだけなら問題ないでしょう。複数のImageViewと一緒に回転させるから、症状が出たのだと思います。これもImageViewのように、同じサイズのViewで囲むと直るのか、後でテストする予定です。

こんな風に格闘している間に、Titanium Mobile 2.0.1GAと同2が来ました。もしかして、今まで遭遇した症状が直っているのでしょうか。まあ、それ以外の部分でのバグ取りがまだでしょうから、さっそく使うわけにもいきませんね。Titanium Mobileは非常に開発しやすいだけに、今までの症状が2.0で消えることを、強く願っています。

2012年4月17日火曜日

画面回転で画像だけ変な位置に

Titanium Mobileを使った2つめのアプリも、必要な機能はすべて作り終わりました。画像をじっくり見せるアプリなので、あとは中身の画像を用意するだけです。これは別途に作業するため、開発は一旦終了。このアプリはiPad専用なのですが、ミニ版としてiPhoneに移植することになりました。iPad版は横長での使用に限定しましたが、iPhone版は縦長でも使用するという話に。というわけで、画面回転に対応させる必要が生じました。そこで新たな問題と遭遇。その顛末、解決までの道のりを書きます。

 

画面の回転に対応させるのは、とても簡単でした。「orientationchange」イベントの処理を追加して、そこでWindow内のUI部品のレイアウトを変更します。複数のWindowがあり、それぞれにレイアウト変更の処理を加えました。メモリー消費を考慮して、ウィンドウは閉じたらメモリー解放する形にしています。画面回転のイベント処理もクリアーする必要から、addEventListenerで追加した処理を、removeEventListenerで削除しなければなりません。削除しやすいように、イベント処理は名前付きの関数を利用します。次のような形で。

// 画面回転の処理
function changeOrientF() {
    var orient = Ti.Gesture.orientation; // 関数で引数eを使わないように、このプロパティで縦横を判断
    if (orient == Ti.UI.PORTRAIT || orient == Ti.UI.UPSIDE_PORTRAIT) {
        view1.top = 100;
        view1.left = 0;
        ...
    } else if (orient == Ti.UI.LANDSCAPE_LEFT || orient == Ti.UI.LANDSCAPE_RIGHT) {
        view1.top = 0;
        view1.left = 200;
        ...
    }
}

考え方としては、UI部品をグループ分けして、それぞれ別なViewに貼り付けます。画面の縦横回転の際に、View単位で位置を調整してレイアウトを変更するようにしました。この考え方で、まったく問題ないはずでした。

 

いろいろなWindowで縦横回転を試すと、画像を入れたImageViewだけが、回転後に変な位置に表示されます。設定する値が間違ってるのかと思い、画面上にデバッグ用ボタンを追加し、回転アニメーションが終わってから、ImageViewのtopとleftの値を表示させました。すると、両方の値は正常に設定されていて、それとは関係ない位置にImageViewが表示されているのです。当然、明らかなバグです。しかし、どこのバグでしょうか。iOSか、Titanium Mobileか。

解決しなければならないので、症状を調べてみました。何十回と回転させると、表示させる位置にはパターンがあるようです。iPhoneの回転位置は4つあり、それぞれで決まった位置にImageViewが表示されました。また、何十回も続けて回転させると、ImageViewに表示される画像が少しずつ劣化していきます。縦位置と横位置ではImageViewの大きさが違い、一旦小さくなってから大きくなるとき、小さくなった画像を拡大して表示している様子でした。つまり、もとの画像ファイルを毎回読み込んでいるのではなくて、画面上に表示した画像を再利用して、回転したり拡大縮小しているというわけです。これらはすべて、アニメーション機能が動いていて、画質重視ではなく効率重視で設計されているのでしょう。

 

いろいろなWindowで観察していたら、ImageViewを1つしか使っていないと、正常な位置に表示されると分かりました。しかし、複数使っているWindowでも、1つ以外は下に隠れていて、画面上には表示されていません。そんな使い方のWindowでも、ImageViewが複数あると、すべてのImageViewが変な位置に表示されます。下に隠れているはずのImageViewまで、一部が表示されてしまうのです。そう、とんでもない症状です。

そこで、次のように考えました。回転し始めたときは、一番手前のImageViewだけ大きさと位置を変更し、他の下に隠れているImageViewは、時間差攻撃で大きさと位置を変更したら良いかも知れないと。JavaScriptは、次のようにしました。

// 1つだけImageViewを変更する
function changeOrientF() {
    var orient = Ti.Gesture.orientation;
    if (orient == Ti.UI.PORTRAIT || orient == Ti.UI.UPSIDE_PORTRAIT) {
        imgView1.height = 240;
        imgView1.width = 320;
        ...
    } else if (orient == Ti.UI.LANDSCAPE_LEFT || orient == Ti.UI.LANDSCAPE_RIGHT) {
        imgView1.height = 320;
        imgView1.width = 427;
        ...
    }
    setTimeout(changeOrient2F, 200); // 0.2秒後に動かす
}
// 残りのImageViewを、時間差を付けて変更する
function changeOrient2F() {
    var orient = Ti.Gesture.orientation;
    if (orient == Ti.UI.PORTRAIT || orient == Ti.UI.UPSIDE_PORTRAIT) {
        imgView2.height = 240;
        imgView2.width = 320;
        ...
    } else if (orient == Ti.UI.LANDSCAPE_LEFT || orient == Ti.UI.LANDSCAPE_RIGHT) {
        imgView2.height = 320;
        imgView2.width = 427;
        ...
    }
}

この0.2秒という時間差は、回転アニメーションの時間を見て決めました。最初は0.3秒にしたのですが、少し余裕がありそうに見え、0.2秒に変更して問題なかったので0.2秒に落ち着きました。実際のアニメーション時間より、少し短いかも知れません。結果は、見事に成功でした。ImageViewが正常な位置に表示されます。

しかし、ここで安心してはいけません。あくまで、シミュレータ上での成功だけです。実機に転送して、最終確認をしなければ。さっそく転送した試したところ、見事に失敗でした。最初にシミュレータ上で乱れた位置とは、また別な位置にImageViewが表示されます。振り出しに戻りました。この時点で、少し凹みましたね。いったん成功したと思ったわけですから。

 

こんなときは、上手い珈琲でも飲んで一休みです。珈琲を飲みながら、別な対処方法を思い浮かべます。ImageViewだけ乱れるなら、ImageViewとまったく同じ大きさのViewを用意して、ImageViewを囲むようにして使ったらどうだろうか。さっそく、次のようなJavaScriptを作って試しました。

// ImageViewを定義する部分
var view1i = bbb.createViewF(view1, 240, 320, 120, 0); // 入れ物のView
var imgView1 = bbb.createImgViewF(view1i, strPhotoName, 240, 320, 0, 0);

// 1つだけImageViewを変更する、画面回転の処理
function changeOrientF() {
    var orient = Ti.Gesture.orientation;
    if (orient == Ti.UI.PORTRAIT || orient == Ti.UI.UPSIDE_PORTRAIT) {
        view1i.height = 240;   // 入れ物のViewを変更
        view1i.width = 320;
        view1i.top = 120;
        view1i.left = 0;
        imgView1.height = 240; // ImageViewは、大きさだけ変更
        imgView1.width = 320;
        ...
    } else if (orient == Ti.UI.LANDSCAPE_LEFT || orient == Ti.UI.LANDSCAPE_RIGHT) {
        view1i.height = 320;   // 入れ物のViewを変更
        view1i.width = 427;
        view1i.top = 0;
        view1i.left = 53;
        imgView1.height = 320; // ImageViewは、大きさだけ変更
        imgView1.width = 427;
        ...
    }
    setTimeout(changeOrient2F, 200); // 0.2秒後に動かす
}

入れ物のViewと、入れるImageViewは、まったく同じ大きさにします。同じ大きさなので、ImageViewのtopとleftはゼロのまま固定。表示位置の変更は、入れ物となるViewのtopとleftの値で設定するわけです。

さて結果ですが、まずはシミュレータ上で試すと成功。ここで安心してはいけません。次に、実機へ転送して動作確認。やりました。大成功です。変な位置に表示される症状が出なくなり、本来の位置に表示されました。とりあえず、良かったです。

 

Titanium Mobileで開発を始めて、まだ3つめのアプリですが、いろいろと問題に遭遇しました。これからも遭遇しそうです。最初に作ったアプリでは、WebViewで苦労させられました。最近は、ImageViewで苦労しています。ここで以前に書いた、変更前の状態が一瞬だけ表示される症状も、ボタンやラベルで発生することはなく、WebViewとImageViewではほぼ確実に発生します。WebViewも、HTMLの評価結果を画像として表示していると捉えられ、画像を表示するViewが、Titanium Mobileの鬼門かも知れません。

開発効率を重視してTitanium Mobile/Desktopを選びましたが、意外に苦労が多くて困惑気味です。前からJavaやCを使っているので、Objective-Cでも苦労しないと思います。Xcodeに移行したら、今回のような問題が発生しないのであれば、移行する価値はありますね。というわけで、ちょっと悩み中です。

 

追記:表示する位置が変になる症状は、ImageViewだけではありませんでした。Labelでも発生することを発見しました。同じ対策が有効かどうか、あとで確認する予定です。

2012年4月13日金曜日

Viewはshowしたときに描き直されるもの?(続き)

何回か前に、「Viewはshowしたときに描き直されるもの?」のタイトルで投稿した内容に関して、実験しながら少し調べてみました。その辺の話をまとめて書きます。

 

そもそもの始まりは、ウィンドウ内のレイアウトを何種類かに変更する目的で、ImageViewやLabelの表示位置を変更したときの動きでした。ViewをhideしてからUI部品の表示位置や中身を変更すると、Viewをshowしたときに、変更前の状態が一瞬表示されてしまう問題です。それを回避しようと、Viewを限りなく透明にしたり、不透明へ戻すときに時間を遅らせて対応しました。

じゃあ、View上のUI部品の位置を変えなかったら、前の状態が一瞬表示される問題は発生しないのか。そんな疑問が残ったので、調べるために実際に動くサンプルを作って実験しました。まず、複数の固定レイアウトViewを用意して、UI部品の位置を変えない方法です。これでも、UI部品の中に表示するテキストや画像は変えますから、Viewをshowしたときの内容は、前に表示した内容と異なります。試した結果ですが、やはり変更前の内容が一瞬だけ表示されました。必ず表示されるわけではなくて、表示されることもあるという感じです。どの程度の割合で発生するかは、動作している環境によって変わりそうですが、意外に多いなと思いました。

次に試したのは、Viewの代わりにWindowを使う方法です。Viewの影響が生じないようにと、UI部品はWindowに直接addしました。Windowなので、showとhideではなくopenとcloseで、表示と非表示を切り替えます。この実験でも、UI部品の内容を変更して再表示し、古い内容を一瞬でも表示されるかを確かめます。気になる結果ですが、やはり同じでした。変更前の内容が、一瞬表示される症状が発生します。これも必ずではなく、たまに発生するという感じでした。

このような結果は十分に予測できました。UI部品を表示している部分のアルゴリズムが同じであれば、UI部品をaddする対象を変えたとしても、同じような動きになって当然です。やっぱり同じだったと確認できたのですが、本当は違ってほしかったところです。もし違えば、回避策の1つとして利用できたのですけど。

 

今回は複数の方法で同じ症状が発生しましたが、解決策がないわけではありません。UI部品に前の状態を作らなければ良いのです。表示する前に毎回、古いUI部品を削除して、新しいUI部品を生成する方法です。これなら前の状態が存在しないため、一瞬でも表示される症状は起こりえません。ただし、別な注意点が生まれます。再表示する度に、古いUI部品を捨てていくため、きっちりとメモリーを解放する必要があります。この辺の注意は、慣れている人なら問題ないでしょう。

個人的にですが、本来は必要ないのに、UI部品を次々に生成する方法は嫌いです。OS、開発ツール、自分のプログラムのどこかのバグに当りそうで、メモリーリークの危険度が増します。採用する気がないので、実験での確認はしませんでした。最初に対策した方法で、問題なく動いてますからね。

2012年4月9日月曜日

遅い処理での小さな心遣い

今回は、Titanium Mobileを使って開発しているアプリでの、ちょっとした心遣いのお話です。前回の投稿で、UI部品が100個以上にもなるViewの話が登場しました。実機でテストしていると、表示されるまでの遅さが気になります。たった2秒弱なのですが、表示されるまでの間に「あれ、動いてないのかな」と感じてしまいます。おそらく、人によっては画面をタップしてしまうでしょう。何か対策を打つのが、良い選択ではないかと考えました。

実際に処理中ですから、ActivityIndicatorを表示させるのが適切な選択でしょう。ActivityIndicatorが回っているのを見せれば、「処理してますから、少し待ってくださいね」と伝わります。ActivityIndicator自体は白いので、四角いグレーのViewを用意し、その中央で回ってもらいましょう。これも、いつものように生成関数を用意して、処理する側では1行で生成しました。JavaScriptは、次のようになります。

// 処理中を伝えるView表示
var view9 = bbb.createViewF(win, 256, 256, 220, 220);
view9.backgroundColor = '#ccc';
var activityIndicator = bbb.createActIndcF(view9, 110, 110);
activityIndicator.show(); 
// 本来のViewを生成(Viewをhideして多数のUI部品を追加)
var view1 = bbb.createViewF(win, 768, 768, 0, 0);
view1.hide();
var lblTitle1 = bbb.createLblF(view1, 'Viewタイトル', 24, 'left', 24, 'auto', 20,20);
...
// 処理中Viewを消す(削除する)
win.remove(view9);
// 本来のViewを表示
view1.show();

本来のview1を生成する前に、処理中だと伝えるview9を真っ先に生成して、そこにActivityIndicatorを追加してshowします。これで処理中の表示が始まりました。続いて、本来のview1とUI部品を生成します。ここが、一番時間のかかる処理ですね。生成が終わったら、処理中表示のview9をウィンドウから削除して、本来のview1をshowします。

上記のJavaScript全体は関数として呼び出す形なので、view1を生成すると終了します。処理中表示のview9は、ウィンドウから削除していますし、関数内の変数も関数の終了で消えてますから、view9で使ったメモリーは解放されるというわけです。本来のview1だけが画面に表示され、動き続けます。

実際に初代iPadで動かしてみると、受ける印象が全く違います。256x256サイズの四角いグレーの中に、ActivityIndicatorが回っているだけですが、待たされているという感じはぜんぜんありません。ActivityIndicatorが動いていて、それを眺めてしまうのが原因でしょうか。待っている際のイライラ感が完全に消えてしまいました。とりあえず、大成功のようです。

2012年4月2日月曜日

UI部品数が多いView表示の高速化

Titanium Mobileに限らず、iOS用のアプリ開発では、シミュレータで動作確認した後、実機に転送して確かめます。実機での動作で問題となるのが、シミュレータよりも遅い点です。開発に使っているのはMacBook Airの13インチ(Intel Core i5, dual 1.7GHz)で、Macとしては遅いほうに分類されます。そんなマシン上のエミュレータでさえ、iPadの実機よりも高速なのです。実機上のテストで遅いと、なんとか改良しなければなりません。

 

アプリでは複数のWindowやViewを扱いますが、UI部品が増えるほど描画が遅くなります。その限界は意外に少なく、View上に数十点のUI部品を付けると、気付く程度には遅くなります。今回は理由があって、あるViewだけは100個以上のUI部品を用いました。そのViewだけは、明らかに遅いです。

今回のアプリで描画が遅いのには、少し別な理由もあります。メモリーを節約しようと、描画が終わったらViewを破棄して、再度表示するときでも新しく生成し直しているのです。つまり、Viewを表示するたびに、UI部品まで含めて生成しています。このようなViewを、1つのWindow上で複数切り替えながら、使い分けています。Window上の一部だけ切り替えるため、Viewを切り替える方法がベストなのです。

生成するのがViewでなくWindowならば、openやcloseのタイミングで表示内容を描きます。しかしViewの場合は、openやcloseが用意されてなくて、別な方法で似たような機能を実現しなければなりません。それをせずに毎回生成すると、UI部品を1つずつ足しながら描いていくため、どうしても表示が遅くなってしまいます。

Viewでのopenやcloseの代わりは、showとhideでしょう。windowなら、最初からcloseした状態で生成されますが、Viewはshowした状態で生成されます。これを防ぐために、Viewを生成した直後にhideするか、hideした状態で生成します。試してみましたが、直後にhideする方法でも問題ないようです。通常は、Viewをshowした状態で生成するような関数を使ってますから、直後にhideする方法で問題ないなら、生成関数が1つで済むため都合がよいのです。具体的なJavaScriptは、次のようになります。

// view1を生成直後にhideする
var view1 = bbb.createViewF(win, 768, 768, 0, 0);
view1.hide();
// UI部品を加える
var label1 = bbb.createLblF(view1, 'ラベル', 14, 'right', 20, 100, 10, 580);
var Button1 = bbb.createBtnF(view1, 'ボタン', 24, 40, 200, 180, 150);
// UI部品を加え終わったら、view1を表示させる
view1.show();

この方法を初代iPadで試したところ、UI部品が数十個のViewでは、違いが分かるほど高速化できました。しかし、UI部品が100個以上あるViewでは、高速化にも限界がありました。Viewが現れるまで2秒弱ほど待たされます。それでも、hideせずに描くよりは充分に高速化されています。この状態で我慢してもらうしかないのでしょうね。

初代iPadはRAM容量が少ないため、メモリーを節約するような作り方をしないと不安です。画面表示が少しは高速化できたので、この方法で最後まで進めたいと思います。

2012年3月27日火曜日

Viewはshowしたときに描き直されるもの?

Titanimu Mobileを使って、新しいアプリを開発中です。いろいろと試しながらの開発なので、実際に作る工程よりも、どんな動きをするのか細かな部分の解析に時間を取られています。使い始めて2つめのアプリなので、仕方ないでしょうね。

 

今回は、Viewの描画で困ったことが起こりました。普通にViewを作成して、普通に表示しているだけなら、とくに問題はありません。でも、少し凝った機能が必要になって、ViewにaddしたUI部品のレイアウトを途中で何度も変更する機能を実装しました。UI部品のtopやleftを途中で変更し、1つのViewのままで、複数のレイアウトを実現する機能です。メモリー効率も良さそうですし。

さすがに、表示したままレイアウトを変更するわけにはいきません。Viewをいったんhideして、hideした状態でUI部品のレイアウトを変更し、変更が終わってからViewをshowしたら問題ないだろう考えました。他の工夫(viewをhideしても問題が生じない工夫)と組み合わせる必要はありますが、Viewに関するJavaScriptは、次のようになります。

view1.hide();      // view1を非表示にします
label1.top = 40;   // view1上のUI部品のプロパティを変更して、画面上のレイアウトを変えます
label1.left = 30;
imgView.top = 120;
...
view1.show();      // view1を再表示します

これが期待した動きになりませんでした。UI部品のレイアウトを変えてから、Viewをshowした場合は、ほんの一瞬だけ変更前のレイアウトが表示され、直後に新しいレイアウトを描いて安定します。フラッシュバックで一瞬だけ古い画像が表示されたような感じです。バグでしょうか、仕様なのでしょうか。こんな使い方をしている人がいないためか、検索しても情報は見付かりませんでした。普通に考えると、表示する内容を内部で作り終わってから、画面に描画するように作るのではと思います。でも、動きから推測するに、そうなってはいないようです。

真剣に困りました。この方法が使えないと、複数のレイアウトのViewを用意しておき、それぞれに値を設定し直してから切り替える必要があります。レイアウトを増やす場合も、新しいViewを追加しなければなりません。美しくないですね。何とか回避できないかと、かなり悩みました。

悩んだ結果、1つ思い付きました。透明度を変えるopacityを使えないかと。opacityの値で限りなく透明にした状態でViewを描かせ、直後に不透明状態に戻せば、フラッシュバックが消えるのではないかと。完全に透明にしてしまうと、描く処理が開始しないと思い、限りなく透明な値として0.001を選んでいます。さっそく、次のようなJavaScriptで試しました。

view1.hide();          // view1を非表示にします
view1.opacity = 0.001; // view1を限りなく透明にします
label1.top = 40;       // view1上のUI部品のプロパティを変更して、画面上のレイアウトを変えます
label1.left = 30;
imgView.top = 120;
...
view1.show();          // view1を再表示します

結果は、惨敗でした。何も変わらず、フラッシュバックが再現されました。まだ、あきらめません。限りなく透明にする位置が悪いのではないかと考えました。hideしてから限りなく透明にしても、その状態を描いていません。限りなく透明にするのを、hideする前に行えば、表示されている時間内に限りなく透明で描いたことになり、有効だと考えました。試したJavaScriptは、次のような形です。

view1.opacity = 0.001; // view1を限りなく透明にします
view1.hide();          // view1を非表示にします
label1.top = 40;       // view1上のUI部品のプロパティを変更して、画面上のレイアウトを変えます
label1.left = 30;
imgView.top = 120;
...
view1.show();          // view1を再表示します

またまた惨敗しました。ここでいったん敗北宣言です。こんなときこそ、美味しい珈琲を飲んで休息です。時間を置いてから、再び考えました。もしかしたら、時間差攻撃が有効かもと。Viewをshowした直後にopacityを戻しているからダメなので、少し時間を経過してからなら大丈夫ではないかと。限りなく透明に設定する処理をhideの後ろに戻し、次のJavaScriptで試しました。

view1.hide();          // view1を非表示にします
view1.opacity = 0.001; // view1を限りなく透明にします
label1.top = 40;       // view1上のUI部品のプロパティを変更して、画面上のレイアウトを変えます
label1.left = 30;
imgView.top = 120;
...
view1.show();                  // view1を再表示します
setTimeout(resetOpacityF, 50); // 50ms後に、不透明に戻すfunctionを起動させます
// この関数は、ここで終了

function resetOpacityF(){      // 時間差攻撃で、view1を不透明に戻します
    view1.opacity = 1;
}

今度は、大成功でした。フラッシュバックがまったく出ません。勘で決めた50msですが、時間差攻撃が成功です。最近の高速コンピュータにとっては、50msでも長い時間なのでしょう。もっと短くなるかも知れませんが、不安が増すので今のところ50msで固定です。

以上は、シミュレータでの動きでした。実機ではフラッシュバックが出ないかも知れませんし、この解決方法で実機も大丈夫という保障もありません。ただし、show後に描き直しているという動きから考えて、非常に有効な解決方法です。

開発の基本としては、実機でもシミュレータでも正常に動作することが大事です。実機で動いたからといって、その実機だけかも知れません。機種もOSバージョンも違う環境が、何種類も存在します。すべての環境でテストすることは無理なので、実機でもシミュレータでも動くことを、最低条件とするわけです。また、問題の解決にあたっては、たまたま直ったのではなく、こういう現象だからこうすれば直るはずと、理論的な裏付けのある解決方法が大事です。

今回の問題は苦労しましたが、何とか解決できて良かったです。まだ実機でのテストが残っていますが、最悪の場合でも、時間差の数値を変更するだけで大丈夫でしょう。複数レイアウトのViewを用意することだけは、ぜったいにやりたくないですからね。

 

Titanimu Mobileの経験が浅いので、もしかしたら、別な解決方法があるかも知れません。ご存じのことがいたら、ぜひ教えてください。一応、viewのプロパティなども本家のAPIページで全部見ましたが、それらしいものは見付かりませんでした。唯一使えそうだったのが、opacityというわけです。

2012年3月21日水曜日

シングルコンテキストでの設計方法(6)

Titanium Mobileを使ったアプリ開発において、シングルコンテキストで作る際の工夫の続き、第6回です。作り方をひととおり説明しましたが、これから主流となりそうな「require」の話を取り上げないわけにはいきません。ということで、「require」を使って作る話です。

 

まず最初に、これまでの経緯を簡単に。Titanium Mobileを使うのが初めてだったので、いろいろ調べました。「Ti.include」を使う方法と、「require」を使う方法の2種類があって、将来的には「require」へ移行するという話のようでした。どちらで作るか悩んだのですが、「Ti.include」を使うほうが簡単そうに見えたのと、「Ti.include」も経験したほうが良いだろうとの考えもあり、最初は「Ti.include」で作って、必要なら後から「require」に書き換えればよいかなと判断しました。

実は、作成した業務アプリ、最初はTitanium Desktpoで作成したものです(こちらも、Titanium Desktpoを使うのが初めてでした)。それをiPadに移植しようと、Titanium Mobile用に修正しながら作りました。予想したよりも短期間でアプリは出来上がり、依頼主に見せたところ、以前見せたデスクトップ版よりも、iPad版を速攻で気に入ってしまいました。短期間だけ試してもらった後、依頼主の要望を反映し、機能を少し追加しました。アプリが非常に安定していたため、すぐに本番で使い始めることに。本番で使い始めたら、安定している業務アプリを修正する気にはなりません。こうして「Ti.include」のまま、アプリは本番で使い続けられています。運が良かったのか、トラブルはまったく出てません。

こんな感じで、「require」に書き換える機会を失ってしまいました。とりあえず「Ti.include」で作るかという、私の直感的な(?)判断が招いた結果ですね。「Ti.include」も分かりやすい形で作れるから、なかなか好きなのですが。でも将来は使わないと言われたら、「require」へ移行できるように試しておかないとダメでしょうね。ちゃんと調べて、作り方も決めてあります。

 

いよいよ、ここからが今回の本題です。「require」を使う場合も、今回の3つのレベル分けは同じです。レベル2とレベル3に1文字ずつ割り当てる考え方も、そのまま生かします。実際にJavaScriptをどう作るのか、順番に見ていきましょう。なお、作り方が何種類から選べるため、私が気に入った作り方を選びました。選んだ理由も、少し加えながら解説します。

まずは、レベル1のJavaScriptです。Ti.include方式では全体を即時関数で作りましたが、これは最初にインクルードされたときに実行して、グローバル変数bbに登録するためでした。require方式でも、グローバル変数に登録しますが、登録方法が異なります。そのため、全体を即時関数にする必要はなくなりました。また、このJavaScriptの即時関数の外で、グローバル変数bbを宣言していましたが、これは別な場所に移動しました。JavaScript自体は、次のような形です。

// base.js (レベル1)
exports.createWinF = function(_title){
    return Ti.UI.createWindow({
        title:_title,
        backgroundColor:'#fff'
    });
}
exports.createLblF = function(_text, _fontSize, _textAlign, _height, _width, _top, _left){
    return Ti.UI.createLabel({
        text:_text,
        font:{fontSize:_fontSize},
        textAlign:_textAlign,
        height:_height,
        width:_width,
        top:_top,
        left:_left 
    });
}
exports.createBtnF = function(_title, _fontSize, _height, _width, _top, _left){
    return Ti.UI.createButton({
        title:_title,
        font:{fontSize:_fontSize},
        height:_height,
        width:_width,
        top:_top,
        left:_left
    });
}

Ti.include方式で「dd.createWinF = 」としていたものが、require方式では「exports.createWinF = 」と変えてあります。require方式で外から呼ばれる関数なので、「exports」が必須となります。それを利用する方法ですが、次のような形にしました。

// app.js (メインのJavaScript)
var bb = {}; // グローバル変数
var bbb = {};          // レベル1専用のグローバル変数
bbb = require("base"); // レベル1を使えるようにする
(function() {
    // メインのウィンドウを開く
    bb.win = bbb.createWinF('prod_edit');
    var lblTitle = bbb.createLblF(bb.win, '主メニュー', 36, 'center', 70, 'auto', 20, null);
    var lblMsg = bbb.createLblF(bb.win, ' ', 18, 'left', 30, 500, 700, 200);
    var btnOpenPe = bbb.createBtnF(bb.win, '商品編集', 24, 40, 200, 100, 32);
    btnOpenPe.addEventListener('click', openPeWinF);
    ...
})();

一番の違いは、レベル1専用のグローバル変数を用意したことです。「bb.func_name = 」という形で追加できなくなり、全体を直接「bb」に入れる「bb = require("base");」という形になります。これだと追加するのではなく、置換えになります。一番最初なので置換えでも、後の追加まで正常に動くのでしょうが、気分的にイヤと感じました。そこで別なグローバル変数「bbb」を用意して、そこに入れた(「require」した)わけです。

当然ですが、レベル1をグローバル変数に入れなくても使えます。その場合は、レベル1を使うJavaScript全部で、先頭に「bbb = require("base");」と書かなければなりません。これが面倒なので、グローバル変数に入れたわけです。面倒だと思わなければ、毎回書いても良いでしょうね。

 

続いて、レベル2のJavaScriptです。これも全体を即時関数で囲む必要はありません。また、先頭で入れ物「bb.c」を用意することも不要です。その結果、JavaScriptは次のようになります。

// c_customer.js (レベル2)
// 継続保持させるデータ用の変数
var customar = [];
var custType = [];

// 外部からデータにアクセスするための関数
exports.initDataF = function(id) { ... } // 初期処理
exports.getNameF = function(id) { ... }
exports.addCustomerF = function(name, ... ) { ... }
exports.checkCstmIdF = function(id) { ... }
// 外部からアクセスされない関数
function calcYearF(date) { ... }
// 上記2種類の関数は、別々に分けるのではなく、関係の深いものを近付ける形で混在させる

値を継続的に保持したい変数を宣言して、後に関数を続けます。外部からアクセスする関数だけ、「exports.func_name = 」という形にして、残りは普通の関数として書きます。

Ti.include方式とは違い、使う側で準備が必要です。複数のJavaScriptから使われるため、毎回宣言するのも面倒と思い、メイン処理でグローバル変数に登録してしまいました。

// app.js (メインのJavaScript)
var bbb = {};          // レベル1専用のグローバル変数
bbb = require("base"); // レベル1を使えるようにする
var bb = {};                   // グローバル変数
bb.c = require("c_customer"); // レベル2を使えるようにする
bb.c.initDataF();              // まずは初期化
bb.p = require("p_products");  // 別なレベル2を使えるようにする
bb.p.initDataF();              // 同じく初期化
(function() {
    // メインのウィンドウを開く
    bb.win = bbb.createWinF('prod_edit');
    var lblTitle = bbb.createLblF(bb.win, '主メニュー', 36, 'center', 70, 'auto', 20, null);
    var lblMsg = bbb.createLblF(bb.win, ' ', 18, 'left', 30, 500, 700, 200);
    var btnOpenPe = bbb.createBtnF(bb.win, '商品編集', 24, 40, 200, 100, 32);
    btnOpenPe.addEventListener('click', openPeWinF);
    ...
})();

レベル2の数だけ、グローバル変数「bb」に登録します。これでレベル3のJavaScriptでは、宣言なしで使えるようになりました。使い方もTi.include方式と同じで、「bb.c.func_name()」という形になります。

 

続いて、最後のレベル3です。レベル3は関数が外部から呼ばれない点で、レベル1やレベル2とは異なります。レベル3は、レベル1やレベル2を利用する側だからですね。「Ti.include」で呼ばれるため全体を即時関数にしていましたが、require方式では「exports」で外から呼ばれる方式に変わります。全体が即時関数の形に似せて、全体を1つの「exports」関数に作りました。即時関数と同様に、全部の変数が関数の内側に入る点が好きだからです。これとは違って、普通に関数を分割して書き、オープンする関数だけ外から見えるようにしても構いません。

// pe_prod_edit.js (レベル3)
exports.openWin = function() {
    // pe用の入れ物を用意する(ウィンドウ用)
    bb.pe = {};

    // 画面に表示するためのUI部品
    bb.pe.win = bbb.createWinF('prod_edit');
    var lblMnTitle = cc.createLblF(bb.pe.win, '商品情報編集', 36, 'center', 70, 'auto', 20, null);
    var lblMsg = bb.createLblF(bb.pe.win, ' ', 18, 'left', 30, 500, 700, 200);
    var btnClose = bb.createBtnF(bb.pe.win, '終了', 24, 40, 200, 100, 32);
    btnSave.addEventListener('click', closeWinF);

    // データを内部的に保持するための変数
    var prodScrn = [];
    var idxProd = null;
    var userId = null;

    // イベントを実行したり、データを加工する関数
    function openWinF() { ... }
    function saveDataF() { ... }

    // ウィンドウを閉じる処理
    function closeWinF(){
        bb.pe.win.close(); // ウィンドウを閉じて、
        bb.pe = null;      // UI部品をウィンドウごと解放する
    }

    // 最後に、生成済みのウィンドウを開く
    bb.pe.win.open();
}

関数は呼ばれないので考慮する必要はないのですが、ウィンドウを開く処理や閉じる処理をどのように配置するのか、決めなければなりません。この例の作り方では、JavaScript全体がオープン用関数になっていて、そのなかでウィンドウの生成なども行います。同時に、ウィンドウ上のUI部品へイベント処理を加え、その処理から呼ばれる関数も用意します。それらが終わった最後に、生成したウィンドウを開いています。

ウィンドウを終了するときのために、画面を終了するボタンとして付けてありますが、「戻る」ボタンであったり、何かの処理が終わった直後に閉じる場合もあるでしょう。とにかく、ウィンドウを閉じる機能が必要です。ウィンドウを閉じる関数では、ウィンドウを閉じた後に入れ物「bb.pe」を空にして、作成したウィンドウごと消えるようにしています。メモリー解放のためです。

このように作った画面用JavaScriptは、次のような形で呼び出します。

menu.js (呼び出しボタンの付いた画面のJavaScript)
(function() {
    ...
    var btnOpenPe = bbb.createBtnF(bb.xx.win, '商品編集', 24, 40, 200, 100, 32);
    btnOpenPe.addEventListener('click', openPeWinF);
    ...
    // 商品編集の画面を呼び出す関数
    function openPeWinF(){
        require("pe_prod_edit").openWin();
    }
})();

ウィンドウを呼び出すための関数では、JavaScript「pe_prod_edit.js」を「require」して、ウィンドウを作って開く関数を呼び出します。分かりやすいように別関数としましたが、たった1行のJavaScriptです。

 

以上のように、require方式でも、3つのレベルに分けた設計方法が使えますし、Ti.include方式と同じメリットがあります。世の流れとしてrequire方式に動いている以上、これから作るならrequire方式でしょう。Ti.include方式は、意外にスッキリした構造で好きなんですけどね。というわけで、新しく作る場合は、require方式で作ってください。私も次のアプリを、require方式で作り進めています。将来を約束されている方式で作っておかないと、やっぱり多少は不安がありますからね。

今回で、設計方法の話はいったん終りです。私は凄く気に入っている方法ですが、感じ方は人それぞれだと思います。部分的にでも気に入った点があったら、そこだけでも利用してください。全体の設計以外にも、iPadアプリを作っていて気付いた点がありますから、ぼちぼち書き進めます。

2012年3月19日月曜日

シングルコンテキストでの設計方法(5)

Titanium Mobileを使ったアプリ開発において、シングルコンテキストで作る際の工夫の続き、第5回です。3つのレベルに分けてJavaScriptを作る、という話で進めてます。今回は3つめとなるレベル3の作り方を取り上げますね。

 

レベル3のJavaScriptに入れるのは、MVCモデルのVCに含まれる内容です。基本的には、アプリで使う画面ごとに分けて作ります。画面ごとに2文字以上の英字を割り当てて、それを先頭に入れたファイル名としてJavaScriptを別々に用意するルールでした。たとえば「pe_prod_edit.js」ように。どのJavaScriptも、次のような形になります。

// pe_prod_edit.js (レベル3)
(function() {
    // 名前空間を別にするために、peの入れ物を用意する
    bb.pe = {};

    // 画面に表示するためのUI部品
    bb.pe.win = bb.createWinF('prod_edit');
    bb.pe.win.addEventListener('open', openWinF);
    var lblTitle = bb.createLblF(bb.pe.win, '商品情報編集', 36, 'center', 70, 'auto', 20, null);
    var lblMsg = bb.createLblF(bb.pe.win, ' ', 18, 'left', 30, 500, 700, 200);
    var btnSave = bb.createBtnF(bb.pe.win, '保存', 24, 40, 200, 100, 32);
    btnSave.addEventListener('click', saveDataF);

    // データを内部的に保持するための変数(bb.pe.を付けなくても、varで動くはずですけど...)
    bb.pe.prodScrn = [];
    bb.pe.idxProd = null;
    bb.pe.userId = null;

    // イベントを実行したり、データを加工する関数
    function openWinF() { ... }
    function saveDataF() { ... }
})();

シングルコンテキストの作り方に沿って、これもJavaScript全体を即時関数で作ります。最初がpeの入れ物で、名前空間を別々にするために用います。ここで作る変数も関数もすべてpeに登録しますから、名前が重複しないようにと調べなくて大丈夫です。

次は、画面表示に使うUI部品です。レベル1のJavaScriptで用意した関数を使い、Titanium MobileのUI部品を加えていきます。最初に作るウィンドウだけはbb.pe空間に付けますが、それ以外のUI部品はvarの変数で作って、ウィンドウにaddするだけです。この例では、addまで含んだレベル1の関数を使い、UI部品を生成しています。ウィンドウだけbb.pe空間に付けますが、「bb.pe.win」と短い表記で済むため、ソースコードが長くなりません。他の画面のウィンドウでも「win」と短い名前に統一すれば、「bb.pv.win」や「bb.ce.win」となって、同じように短い名前で使えます。

続いて、このJavaScript内で使うデータ用変数のうち、値を継続して保持する必要がある変数だけ、bb.pe空間に付けます。これも前回と同じで、おまじない的に付けています。本来は、変数がどこかで必ず参照されていて、bb.pe空間に付けなくても動作するはずなのですが、、、、。前回と同様に苦肉の策なので、この部分だけは非推奨ということで。

保持して置く場所として、変数以外に画面表示も使えます。ユーザーが選んだ値は、画面上のUI部品に保持しています。その値を調べることで、変数に持つ必要はなくなります。また、UI部品が保持している値がそのまま使えないときでも、UI部品に新しいプロパティを加えて、そのプロパティに値を保持させる方法が使えます。どちらの方法でも、JavaScript内の変数を持たなくても、持ったと同じ効果が得られます。こういう方法も利用して構わないのではないでしょうか。

レベル3で作る関数は、他のJavaScriptから呼ばれるものは、めったにありません。他の画面と共有する関数はレベル1として作り、その関数へウィンドウやボタンの情報を渡して動かす形にします。そうすると、このJavaScript内で使う関数しか残らないのです。したがって、どれも普通の関数「function func_name() { ... }」の形で作ることになります。

 

外から呼ばれる関数がないので、ここで作った関数を使うJavaScriptの例もありません。当然ですね。逆に、ここの関数内では、レベル1やレベル2で作った関数を多用します。それらを使って、データの作成、加工、保存を実現します。また、新しく作成するデータは、画面に入力した値を取り込んで利用します。

画面上でのエラーチェック機能は、このJavaScriptに入れます。ただし、エラーを判定する処理だけは、レベル1またはレベル2に関数として用意します。その関数を呼び出してエラーかどうか判定し、判定結果を画面に表示します。エラー判定関数をレベル1に作るか、レベル2に作るかは、エラーチェック内容によって決まります。適用業務に関係するならレベル2に、関係しないならレベル1に作るのが基本です。特定の形式の文字列かどうかのチェックなら、適用業務に関係ないのでレベル1の関数となります。逆に適用業務上のエラーチェックは、該当するデータのレベル2JavaScriptに入れます。データごとに、関係するロジックがレベル2の該当箇所に集まるというわけです。

より良い構造で作るのであれば、レベル1のエラーチェック関数を、レベル3から直接呼ばない形が理想です。たとえ特定の形式の文字列かどうかのチェックであっても、チェック目的を表す名前でレベル2に関数を用意し、その中でレベル1のエラーチェック関数を呼ぶようにします。レベル1の関数名は、数値かどうかのチェックを意味する名前に、レベル2の関数名は、どのデータ項目をチェックするか表す名前になるでしょう。このように作れば、レベル2のJavaScriptの中に、エラーチェック内容がすべて集まる形に仕上げられます。

 

こんな感じで作り進みます。作業の順序としては、名前空間の入れ物を用意して、UI部品を作り、内部保持の変数を加えて、関数を書いていく感じですね。必要に応じて、UI部品の表示位置を調整したり、関数の修正や追加を行います。

以上で、3つのレベルの作り方を紹介し終わりました。ここまで書いてきて、レベルという表現(言葉)が適切だったのか、少し迷ってます。JavaScriptを3層構造に分けるという意味で、レベルかなと考えましたが、はたして良かったかどうか。適切な言葉って、意外に難しいですね。

シングルコンテキストでの作り方を調べたとき、将来的に「Ti.include」が使われなくなって、「require」を使う話がありました。最後に、「require」の話を別投稿で取り上げましょう。

2012年3月18日日曜日

シングルコンテキストでの設計方法(4)

Titanium Mobileを使ったアプリ開発において、シングルコンテキストで作る際の工夫の続き、第4回です。です。3つのレベルに分けてJavaScriptを作る、という話でした。今回は、その中のレベル2の作り方を取り上げます。

 

レベル2のJavaScriptに入れるのは、MVCモデルのMに含まれる内容です。適用業務で扱うデータを、種類ごとに分けて作ります。顧客情報、商品情報、販売情報など、情報ごとに分けて別々のJavaScriptとして作ります。それぞれに英字1文字を割り当て、その文字を先頭に入れたファイル名を付けるというルールでした。たとえば「c_customaer.js」のように。どのデータのJavaScriptも、次のような形になります。

// c_customaer.js (レベル2)
(function() {
    // 名前空間を別にするために、cの入れ物を用意する
    bb.c = {};

    // 継続保持させるデータ用の変数(bb.c.を付けなくても動くはずですけど...)
    bb.c.customar = [];
    bb.c.custType = [];

    // 外部からデータにアクセスするための関数
    bb.c.getNameF = function(id) { ... }
    bb.c.addCustomerF = function(name, ... ) { ... }
    bb.c.checkCstmIdF = function(id) { ... }
    // 外部からアクセスされない関数
    function calcYearF(date) { ... }
    // 上記2種類の関数は、別々に分けるのではなく、関係の深いものを近付ける形で混在させる
})();

シングルコンテキストの作り方に沿って、JavaScript全体を、即時関数で作ります。最初に用意するのが、cの入れ物です。ここで作る変数も関数もすべてcに登録しますから、他と名前が重複しないように考える必要はありません。JavaScriptの予約語などと重複しないように注意するだけで済みます。Titanium Mobileで使われている名前は、Titaniumオブジェクトに付けてあるので、同じ名前でも重複扱いにはなりませんね。

続いて、このJavaScript内で使うデータ用変数のうち、値を継続して保持する必要がある変数だけ、bb.c空間に付けます。本来は、変数がどこかで必ず参照されていて、bb.c空間に付けなくても動作するはずなのですが、実際に動かすと、ごく一部の変数だけは消えてしまった様子に。もしかしてと思って付けたら、正常に動きました。自分の使い方が悪いのか、そういう動きが正常なのか、未だに不明です。時間がなかったので、ごく一部の変数だけなんですが、また発生して悩むのは時間の無駄でしょう。仕方がないので、値を継続保持する変数だけは、bb.c空間に付けることにしました。もちろん苦肉の策で、この部分だけは非推奨でお願いしますね。

時間がなかったので、とりあえず動くようにして作業を続けました。そしたら、どの変数で発生したのか忘れてしまって、原因追及ができないでいます。コメントで印を付けておくべきでした。まあ、「急いでるから」とか「時間がないから」と先を急ぐときに起りがちな、残念な失敗の一つでしょう。我ながら情けないです。動いているのを変更して探すのもアレなので、とりあえずこのままかな。

レベル2で作る関数には、外部(レベル2上位層とレベル3)から呼ばれる関数と、即時関数の内部からしか呼ばれない関数に分けられます。外部から呼ばれる関数だけはbb.c空間に付けた形で作り、それ以外を普通の関数(function func_name() { ... })として作ります。これらの関数は、内容を理解しやすいように、関係の深い関数を近くに並べるようにします。外部から呼ばれる関数だけ集めて並べる必要はありません。

 

こうして用意したレベル2のJavaScriptの関数は、レベル2上位層とレベル3のJavaScriptで利用します。bb.c空間に付けてるので、それを付けた形で呼び出すだけです。

// ce_customer_edit.js (レベル3)
(function() {
    bb.ce = {};
    bb.ce.win = bb.createWinF('custm_edit');
    ...

    var cstmId = lblCstmId.text;
    if (bb.c.checkCstmIdF(cstmId)) {
        var strCstmName = bb.c.getNameF(numId);
        ...
    }

    ...
})();

用意した空間名「bb.c.」を関数名の前に付けるだけで、それ以外は普通の関数と同じ使い方です。空間名が短いので、それほど邪魔にならないのが救いでしょうか。「bb.c.」すら書きたくない人は、「var c = bb.c」と作れば、「bb.c.」が短い「c.」に置き換えられます。そもそも「bb」は仕方なく付けている接頭語みたいなもので、「c」だけでも意味が通じますからね。私としては、そこまでやる必要がないと思いますけど。

 

レベル2上位層は、レベル3の関数利用とレベル2の関数公開を混ぜたような作り方になります。レベル2で作った関数を、bb.cなどの空間名付きで用いながら、自分が公開する関数を空間名付きで作ります。レベル2上位層にも重複しない空間名が割り当てられているので、関数名を自由に付けられます。レベル2上位層で作った関数の使い方は、レベル2で作った関数と同じです。

ほどほどの長さになったので、続きは別な投稿にしますね。

2012年3月17日土曜日

シングルコンテキストでの設計方法(1)

Titanium Mobileを使い、iPad上で動く業務アプリを作りました。Titanium Mobileを使う最初のアプリだったので、どのような内部構造にするか事前に調べました。出てきたキーワードが、シングルコンテキストです。シングルコンテキストながら、個々のプログラムが影響を及ぼしにくく作るのが、今後の主流になりそうだと感じました。何人もの人が主張しているほど、人気の作り方のようです。正しい起源は知りませんが、tweetaniumというサンプルがもとになっているようでした。

詳しい内容は、もう何人もの方が書かれているので、ここで書く必要はないでしょう。「Titanium Mobile」と「シングルコンテキスト」で検索してみてください。たくさんのページが出てきますから、その中から何ページかを読めば理解できるでしょう。いちおう簡単に説明すると、次のようになります。「Ti.include」で読み込みながらシングルコンテキストで作るものの、特別なグローバル・オブジェクトを用意し、それに付加する形でウィンドウや関数を作ります。付加するプログラムは内容で分割し、それぞれ違う名前の場所に加えることで、別々の名前空間を実現します。また、ぞれぞれのプログラム全体を、即時関数として作るのも大きな特徴です。なかなか賢い方法ですね。

 

さて、ここからが本題です。私が作った業務アプリも、同じ構造を採用しました。ただし、「シングルコンテキスト+即時関数」という考え方だけでは、アプリ全体をすっきりした構造にするは不十分でした。さらなる工夫が必要で、その工夫を紹介します。

今回の業務アプリの内容から、特別なグローバル・オブジェクトを「bb」としました。この「bb」オブジェクトにウィンドウや関数を追加するわけですが、どのように分割するかが悩むところです。iPadアプリですから、画面に表示した内容にタッチしながら、作業を進めることになります。何種類かの画面があって、画面ごとに機能が分割されています。画面ごとに分割するのは、理解しやすくて作りやすい考え方でしょう。また、扱うデータごとに分割するという考え方もあります。商品データ、顧客データ、販売データなどによる分割です。加えて、アラートや確認ダイアログのように、システム全体で使うプログラム機能もあるでしょう。この3つを全部入れて、3レベルに分割したら良いと考えました。整理すると、レベル1がプログラム機能、レベル2が扱うデータに関わる処理、レベル3が画面に関係する機能です。

せっかく3レベルに分けたのですから、それらを「bb」に付加するときのルールも用意しました。レベル1では、そのまま追加する。レベル2では、1文字の英字で追加する。レベル3になると、2文字の英字で追加するという具合です。具体的な例を書くと、次のようになります。

// 特別なグローバル・オブジェクト
var bb = {};

// レベル1
(function() {
    bb.alertSp = function(msg) { ... }
})();

// レベル2
(function() {
    bb.c = {};
    bb.c.getName = function(id) { ... }
})();

// レベル3
(function() {
    bb.pe = {};
    bb.pe.strNameTable = [];
})();

加える文字は、0〜2文字と少なくしてあります。文字数が少ないことで、記述しやすくなるのを狙っているからです。参照される頻度は、レベル1とレベル2が圧倒的に多いため、この2つをより少ない文字数に割り当てたわけです。

レベル2と3の文字には、それぞれ意味を持たせたほうが理解しやすくなります。レベル2では、扱うデータごとに異なる文字を割り当てるのですから、データ名の先頭文字が適しています。商品情報ならproductsのpを、顧客の情報ならcustomerのcを、販売情報ならsalesのsをという具合に。レベル3も同様で、画面に対応させた機能を英字2文字に略した言葉が適しています。商品情報の編集画面なら、英語での一般的な表現と言うことでedit productsを略したepとしたくなるでしょう。しかし、お薦めなのは、逆順のpeです(理由は後述)。こうすると多くの場合、1文字目が作業対象を、2文字目が作業の種類を表すことになります。それが理解できると、たった2文字なのに、何をどうしている画面なのか予想できるようになります。もし2文字では足りないと感じる場合は、このレベルを3文字にする方法もありまし、2文字と3文字の混在でも構いません。好きずきで選んでください。

とは言うものの、pやsは競争率が高く、どうしても衝突しがちです。そんな場合は、もっとも重要なものをpやsに割り当てて、それ以外は別な英字に割り当てます。また、環境設定(pref)のように、すべてのアプリで使いそうな機能は、あまり使いそうもない英字(zなど)を割り当てて、アプリが変わっても同じ文字を割り当てられるように決めておきたいものです。

 

分割方法と割り当て文字が決まったら、それに従ってJavaScriptを作成します。当然、対応文字ごとにファイルも分割します。その際に大事なのが、ファイル名です。ファイル名の先頭に、割り当てた文字を入れるのです。たとえば、商品情報を扱うJavaScriptなら、ファイル名を「products.js」とするのではなく「p_products.js」とします。同様にレベル3でも、「prod_edit.js」ではなく「pe_prod_edit.js」というように。このように分かりやすい内容なら先頭に付ける意味はありませんが、「z_pref.js」や「ez_pref_edit.js」のような名前なら価値が大きいでしょう。

これら以外にも、ヘルプのような特別な画面もあります。このような画面でも、グローバル・オブジェクトに加える以上は何らかの2文字が必要です。作業名として意味のある1文字を無理して作り出そうとすると、無駄に悩んでしまいます。ここは気軽に、helpを単純に2文字に略した「hp_help.js」で構わないと思います。何事も柔軟にですね。

割り当て文字をファイル名に入れると、ファイル名の一覧表にも表示されます。その結果、割り当てた文字を否応なく記憶されられてしまいます。頑張って覚えるという感覚ではありません。何の努力もせず覚えてしまいますから、「bb.h」とか「bb.ez」とかが探す必要もなくなります。だからこそ、ファイル名に含めることが大事なのです。また、ファイル名を見ただけで、使っていない文字並びも簡単に分かります。新しい2文字を追加する際にも、今まで使った2文字を調べる必要はありません。

さて、2文字の場合、ep(edit_prod)ではなくpe(prod_edit)にした理由の話です。作業対象を前に置くと、ファイル一覧にファイル名が並んだとき、同じ対象を扱うJavaScriptが連続して並びます。ファイル一覧が自動的に整理された感じに見え、管理しやすくなります。というわけで、作業対象を前に置いた割り当て方法を強くお薦めします。

base.js
c_customer.js
ce_customer_edit.js
cv_customer_view.js
cvp_customer_view_prod.js
hp_help.js
p_products.js
pe_prod_edit.js
pv_prod_view.js
z_pref.js
ze_pref_edit.js

この中で先頭にある「base.js」が、レベル1のJavaScriptです。先頭に並んでほしかったので「base」という言葉を選びました。本当は「a」で始まる言葉を使いたかったのですが、思い浮かびませんでした。「base」ならば「b」で始まるファイル名の中でも一番先頭に来そうですし、言葉の意味としても適していそうなので、とりあえず良いかなと思いました。

ここまででも、予想したより長くなってしまいました。続きは、別な投稿として書きます。

 

追記:言葉の間違いを直すため数文字変更しただけなのに、投稿した日付も更新され後ろに移動しました。bloggerのバグなのか仕様なのか、困ったものです。この追記で、また新しい日付に変わるのでしょうか。投稿順に並ばなくなりましたが、直すのが大変なので、このままにしましょうかね。

2012年3月16日金曜日

シングルコンテキストでの設計方法(3)

Titanium Mobileを使ったアプリ開発において、シングルコンテキストで作る際の工夫の続き、第3回です。3つのレベルに分けてJavaScriptを作る、という話でした。今回は、その中のレベル1の作り方について、少し詳しく書きます。

 

レベル1のJavaScriptに入れるのは、適用業務に無関係なプログラム上の機能です。代表的なものとして、共通のアラート、確認を求めるダイアログ、タイマー処理、簡単なアニメーションなどです。アニメーションというのは、ウィンドウのオープンやクローズの際のアニメーションだけではありません。文字入力するときにソフトキーボードが出て、画面の下側にある入力フィールドが見えなくなる問題への対応です。TextFieldなどをViewの上に置き、Viewごと上に移動する機能を実現します。

こういった機能も必要ですが、もっとも大事なのは、UI部品の記述を簡単にすることです。Titanium Mobileで作り始めて面倒に感じるのが、Ti.UI.createXXXとして記述するUI部品の生成処理でしょう。行数が増えるだけでなく、数多くのUI部品を付けるほど、ソースコードが長くなってしまいます。レベル1のJavaScriptとなる「base.js」に、UI部品を生成する関数を用意します。

// base.js (レベル1)
bb = {};
(function() {
    bb.createWinF = function(_title){
        return Ti.UI.createWindow({
            title:_title,
            backgroundColor:'#fff'
        });
    }
    bb.createLblF = function(_text, _fontSize, _textAlign, _height, _width, _top, _left){
        return Ti.UI.createLabel({
            text:_text,
            font:{fontSize:_fontSize},
            textAlign:_textAlign,
            height:_height,
            width:_width,
            top:_top,
            left:_left 
        });
     }
    bb.createBtnF = function(_title, _fontSize, _height, _width, _top, _left){
        return Ti.UI.createButton({
            title:_title,
            font:{fontSize:_fontSize},
            height:_height,
            width:_width,
            top:_top,
            left:_left
        });
    }
})();

レベル1の関数なので、用意したグローバル・オブジェクトの「bb」に、直接加えています。これらは、外から呼ぶことが可能なようにと「bb」に加えているわけです。逆に、base.js内でしか呼ばれない関数は、普通に「function func_name() { ... }」の形で書いて構いません。

こうした用意した関数をレベル3のJavaScriptで利用すると、次のような感じになります。

// pe_prod_edit.js (レベル3)
(function() {
    bb.pe = {};
    bb.pe.win = bb.createWinF('prod_edit');
    var lblTitle = bb.createLblF('商品情報編集', 36, 'center', 70, 'auto', 20, null);
    bb.pe.win.add(lblTitle);
    var lblMsg = bb.createLblF(' ', 18, 'left', 30, 500, 700, 200);
    bb.pe.win.add(lblMsg);
    var btnSave = bb.createBtnF('保存', 24, 40, 200, 100, 32);
    btnSave.addEventListener('click', saveDataF);
    bb.pe.win.add(btnSave);

    function saveDataF() { ... }
})();

普通に記述する方法よりも、格段に短くなったと感じたのではないでしょうか。UI部品の数が多くなるほど、その効果の大きさに感激します。無駄な記述が格段に減り、アプリ全体でのソースコードの量も大きく減ります。また、生成する処理の行が長くならないので、別に分ける必要もなく、生成内容と処理を近くに書けます。

これでもまだ不満、addすら何度も書きたくないという人には、生成する関数にaddも含めてしまう方法があります。

// base.js (レベル1) addを追加したもの
bb = {};
(function() {

    bb.createLblF = function(_view, _text, _fontSize, _textAlign, _height, _width, _top, _left){
        var lbl = Ti.UI.createLabel({
            text:_text,
            font:{fontSize:_fontSize},
            textAlign:_textAlign,
            height:_height,
            width:_width,
            top:_top,
            left:_left 
        });
        _view.add(lbl); // addします
        return lbl;
     }
    bb.createBtnF = function(_view, _title, _fontSize, _height, _width, _top, _left){
        var btn = Ti.UI.createButton({
            title:_title,
            font:{fontSize:_fontSize},
            height:_height,
            width:_width,
            top:_top,
            left:_left
        });
        _view.add(btn); // addします
        return btn;
    }
})();

これを利用する側のJavaScriptは、前よりもさらに短くなります。

// pe_prod_edit.js (レベル3) add対応版
(function() {
    bb.pe = {};
    bb.pe.win = bb.createWinF('prod_edit');
    var lblTitle = bb.createLblF(bb.pe.win, '商品情報編集', 36, 'center', 70, 'auto', 20, null);
    var lblMsg = bb.createLblF(bb.pe.win, ' ', 18, 'left', 30, 500, 700, 200);
    var btnSave = bb.createBtnF(bb.pe.win, '保存', 24, 40, 200, 100, 32);
    btnSave.addEventListener('click', saveDataF);

    function saveDataF() { ... }
})();
UI部品の標準設定のまま使う限り、addまで含めて1行で済んでしまいます。たいへん見やすいソースコードになりますね。私が作った業務アプリは、addを含まない生成関数を使いました。次に作るものは、addを含んだ生成関数を使おうと思っています。

addを含めた作り方は良いけど、addしない使い方も可能にしたいというワガママな要望がある場合は、次のように条件式を入れます。

// base.js (レベル1) addワガママ版
bb = {};
(function() {

    bb.createBtnF = function(_view, _title, _fontSize, _height, _width, _top, _left){
        var btn = Ti.UI.createButton({
            title:_title,
            font:{fontSize:_fontSize},
            height:_height,
            width:_width,
            top:_top,
            left:_left
        });
        if (_view) _view.add(btw); // 条件付きadd
        return btn;
    }
})();
// pe_prod_edit.js (レベル3) addワガママ版を利用
(function() {
    bb.pe = {};

    // 普通にaddするボタン生成
    var btn1 = bb.createBtnF(bb.pe.win, '保存', 24, 40, 200, 100, 32);
    // addしないボタン生成
    var btn2 = bb.createBtnF(null, '隠れ保存', 24, 40, 200, 100, 32);

})();

こんな感じで、いろいろとアレンジできますね。addしない生成にどんな使い道があるかは別にして。また、どんなアレンジを加えるかは、最終的に好みの問題でしょう。

 

生成関数を作る際の考慮点も、いくつか紹介します。生成関数で作るUI部品の設定は、アプリ内で一番標準的な形を選びます。そして、標準以外の設定で使いたいUI部品は、生成関数で作った直後に、好みの形に設定を変更します。こうすることで、アプリ全体の記述がもっとも少なくなるはずです。また、標準設定を変更するコードが加わることで、標準設定から何を変えたのか、すぐ理解できるようになります。このような関数を使わず、すべての設定が並んでいる記述だと、どの設定が他と違っているのか、非常に見付けにくいですから。

// pe_prod_edit.js (レベル3) 標準設定を変更する
(function() {

    var lblMsg = bb.createLblF(bb.pe.win, ' ', 18, 'left', 30, 500, 700, 200);
    lblMsg.color = '#888'; // 標準設定を変更

    var btnSave = bb.createBtnF(bb.pe.win, '保存', 24, 40, 200, 100, 32);
    btnSave.addEventListener('click', saveDataF); // これも変更の一つ

})();

アプリによっては、2種類の設定を多用することもあるでしょう。そんなときは、生成関数を1つに制限せず、2種類の設定を持った別々の生成関数を用意して使い分けます。2種類の設定を別々に持てるので、設定の変更もそれぞれ一箇所で済みます。もちろん、生成関数が1つの場合も、標準設定の変更が一箇所で済みますね。

 

引数の並び順も、できるだけ標準化したい点です。add版で考えると、add対象のビュー、ボタンなどの名前を最初に入れるでしょう。それ以降も、できる限り同じ値を入れて、先頭からの並び順を統一したくなります。候補としては、縦横の大きさと位置の4つの値が一番の候補でしょう。

しかし実際に試してみると、この4つの値は、もっとも右側に並べるのがベストです。UI部品を生成した行は、他の行よりも長くなります。すると、生成行の右側の値だけが、何もない空間に存在するような感じに見えるのです。その場所に4つの値を置くと、4つの値が見付けやすくなります。この4つの値は、UI部品の大きさと位置を調整するために何度も変更することが多く、少しでも見やすい形で作っておきたいのです。

というわけで、引数のお薦め並び順は、add対象のビュー、ボタンなどの名前、その他の設定値、縦横の大きさと位置の4値となります。最後の4つの値の並び順を統一することは、言うまでもありませんね。

Titaniumu Studioを使っていると、関数の引数を表示してくれるので、引数の並び順を覚えておく必要はありません。実際には、ラベルやボタンを生成した行をコピー&ペーストして、必要な箇所だけ引数を変更することが多いので、Studioの引数表示すら、めったに使いませんけどね。

 

以上のような形でレベル1のJavaScriptに、ウィンドウ、ビュー、タブビュー、ラベル、ボタン、テキストエリアなど、通常なら10種類ほどの生成関数を用意します。Ti.UI.createXXXとして作るものは、基本的にすべて対象となります。あとは、レベル3のJavaScriptでガンガン使うだけです。レベル3の記述が簡素化され、見やすいプログラムになるでしょう。

メリットを整理すると、次のようになります。

・UI部品を生成する長い記述がたった1箇所で済む(アプリ全体での記述量も減る)
・個々のUI部品生成は1行で済み、簡潔で読みやすくなる
・生成処理を別に分ける必要もなくなり、生成設定と処理を近くに置ける
・UI部品の大きさや表示位置の数値が右側に飛び出した感じで、変更するときに見付けやすい
・UI部品の標準設定を変更するとき、一箇所だけ直せばよい
・標準設定のまま使わないUI部品が、何を変えたのか見分けやすい

今回は、短いとはいえソースコードをいくつも貼り付けたので、全体が長くなってしまいました。レベル2とレベル3の作り方は、別な投稿に分けます。

2012年3月15日木曜日

シングルコンテキストでの設計方法(2)

Titanium Mobileを使ったアプリ開発において、シングルコンテキストで作る際の工夫の続きです。3つのレベルに分けてJavaScriptを作る、という話でした。今回は、レベル2(データを取り扱う処理)とレベル3(画面操作に関係する処理)の役割について書きますね。

 

オブジェクト指向でアプリを作る場合、MVCという考え方があります。M(モデル)、V(ビュー)、C(コントロール)という区分けで、アプリの機能を分割する方法です。このMとVとCを、レベル2とレベル3に当てはめて考えましょう。

基本となるデータを扱うレベル2は、Mに該当します。データを処理する機能のほとんどをレベル2に入れるからです。もう1つのレベル3は、VとCに該当します。画面を操作するタイプのアプリでは、VとCの分離が明確になりにくく、両方が混在した形になりがちです。VとCを完全に分離しなくてもアプリは作れますから、ここの分離に強く拘る必要はないでしょう。とくに処理速度が遅いモバイル機器のアプリではね。

 

一番大事なのは、MとVCの分離です。この分離さえ上手に作れれば、将来の機能変更に対して、変更箇所が少なくて済むアプリになります。すっきりした内部構造のアプリになるというわけです。この分離をキッチリ作るように意識しなければなりません。

正確に言うなら、レベル2がMに該当するのではなく、該当すると言えるようにレベル2を作るべきという話です。扱うデータをそのまま解放するのではなく、データの構造を隠蔽して外に出さずに、用意した関数を通してアクセスさせます。オブジェクト指向に慣れているなら、get、set、addなどのメソッドを通じてデータを扱う形を知っているでしょう。それと同じように、より使いやすい機能まで加えて作るということです。

ただし、モバイル機器で実行させる場合、処理能力の低さを考慮する必要もあります。さらに悪いことに、ネイティブのコンパイラ言語ではなく、JavaScriptで動かすわけですから、データ構造の隠蔽を徹底的に行うと、処理が遅いと感じることも起こりうるでしょう。実機で動かした経験をもとに、ある程度の妥協点を見付ける必要もあるようです。この辺は、アプリの処理内容に大きく依存するため、具体的なアプリで判断するしかありません。

Mの中で何種類ものデータを一緒に扱う場合は、データが相互に関係していることも多くなります。これもレベル2に含めましょう。たとえば、顧客情報と商品情報から、特定の顧客が購入した商品の一覧を表示するといった場合です。顧客IDを指定したら、商品IDの一覧を返すといった処理を、レベル2の上位層として追加します。JavaScriptを入れる場所は、bb.cでもbb.pでもなく、新しく1文字のbb.xなどを加えます。

モデルの種類が多いアプリの場合、レベル2の上位層まで加えると、レベル2を1文字で表すのが難しくなるでしょう。そんなときは、レベル2の上位層は2文字にして、レベル3を3文字以上にする手もあります。つまり、1文字はレベル2の基礎層でデータ単体での処理、2文字はレベル2の上位層で複数データ間の処理、3文字以上がレベル3とするわけです。

 

ここまでを読んで、「bb.model」や「bb.ui」と似た話ではないかと思った人がいるかもしれませんね。実は、ここで紹介しているレベル分けの表記方法は、「model」や「ui」の改良版とも解釈できる方法なのです。「model」はレベル2に、「ui」はレベル3に該当します。つまり、「model」と記述する代わりに、「p」や「c」と1文字にすることで実現しています。「bb.model.products」と書かなければならないところを、「bb.p」で済ませられるわけです。同様に、「ui」と記述する代わりに、「pe」や「cv」と2文字にすることで伝えています。「bb.ui.products_edit」と書かなければならないところを、「bb.pe」で済ませられます。

「model」や「ui」は、アプリ内の処理の区分けであって、ソースコード上に何度も何度も記述される必要はありません。ただでさえ長くなりがちな最近のソースコードが、ますます長くなってしまいます。それを防ぐ目的でも、このレベル分けの記述方法が役立ちます。短く簡潔なソースコードを目指すというわけです。

 

以上の話から、3つのレベルは次のように解釈できるでしょう。
・レベル1:基礎層
・レベル2:MVCのM層
・レベル3:MVCのVC層

キリがよいので、ここで一旦切ります。続きは、別投稿にて。