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容量が少ないため、メモリーを節約するような作り方をしないと不安です。画面表示が少しは高速化できたので、この方法で最後まで進めたいと思います。