書評: JavaScript と Titanium ではじめる iPhone/Android アプリプログラミング

@sngmr さんの書かれた Ti 本が 9/14 に発売されました。

JavaScriptとTitaniumではじめる iPhone/Androidアプリプログラミング 【Titanium Mobile SDK 2.1 & Titanium Studio 2.1 対応】

JavaScriptとTitaniumではじめる iPhone/Androidアプリプログラミング 【Titanium Mobile SDK 2.1 & Titanium Studio 2.1 対応】

そしてなんと何もしていないのに献本していただいちゃいました。ありがとうございます!
ということで、感想を書いて紹介させていただきます。

本の内容と感想

他の方も書かれているとおり、本書はスマートフォンアプリ開発をこれから始めたいという人向けの入門書なのですが、

  • フルカラー&スクリーンショットが多数でとっつきやすい
  • 環境構築から必要なことをすべて順を追って説明されていてわかりやすい

といったところが特長になっています。

ほんとに一つ一つのステップが丁寧に説明されている上、手順通りにいかなかった場合も想定される範囲で注意書きなどでフォローアップしており、入門書としてオススメな内容になっています。

特に僕がいいなと思ったのが基本的なパーツの紹介でした。
よく使うラベルやボタン、テーブルビューなどの主要なパーツを一つずつ解説しているのですが
基本情報として

  • Kitchen Sink で対応する画面とコード
  • API リファレンスの URL

を記載しつつ、基本的な使い方を説明されています。

これだけ丁寧に各部品を説明してもらえると、各部品に対しての理解が深まるだけじゃなく
全体像がイメージできて他の部品について知りたくなったときに調べる力も自然につくんじゃないかなと思います。

進化の早い Titanium において Titanium Studio や Appcelerator のサイトのスクリーンショットを多数のせていることは諸刃の剣だと思いますが、
サポートサイトでフォローアップもされるようです。
http://book.mycom.co.jp/support/bookmook/titanium_programming/

まとめ

だれかに Titanium 始めてみたいと言われたら、これからはまずこの本を紹介するようにします!

Titanium SDK をソースからビルドする方法

1.8.2 での既知のデグレの修正をいち早く取り込みたかったので、
Titanium SDK のソースからビルドを試してみた。
ついでに高速化 Tips も。

準備

いくつかインストールしておく必要がある。

SCons

Titanium SDK のビルドに利用しているツール。
http://www.scons.org

Android NDK

C/C++ を利用する場合に必要なもの?
http://developer.android.com/sdk/ndk/index.html

PyYAML

Titanium Studio のコードアシスト用のファイル生成まわりで使っている様子。
http://pyyaml.org

ビルド

まずはチェックアウト。
1.8.2 の場合。

$ git clone https://github.com/appcelerator/titanium_mobile
$ cd titanium_mobile
$ git checkout 1_8_2_GA

次に Android SDK/NDK のパスを設定。
android/build/build.properties を作成して以下の内容を記載。

android.sdk=(Android SDK のパス)
android.ndk=(Android NDK のパス)

あとは以下を実行すると SDK ができる。
PRODUCT_VERSION は生成後の名前。
Titanium Studio とかでの表示名になる。

$ scons android_sdk=(Android SDK のパス) PRODUCT_VERSION=my-1.8.2

完了すると dist ディレクトリに mobilesdk-my-1.8.2-osx.zip としてできあがってる。

高速化

CCACHE を使った高速化がここで紹介されてます。
手元では6分半が1分に!
http://kazkonno.tumblr.com/post/15779298307/building-titanium-mobile-sdk-from-source

Titanium 製アプリで NativeDriver を試す(iPhone 編)

Android アプリに続き、iPhone アプリで NativeDriver を試したメモ。
Titanium 製アプリで NativeDriver を試す(Android 編) - vaccho's blog

Titanium 製アプリで対応する

アプリ側の対応は、Xcode 上でライブラリを読み込んであげるのと
main.m でサーバを起動するようにしてあげれば良い。
http://code.google.com/p/nativedriver/wiki/IOSMakeAnAppTestable

ライブラリを読み込む

Titanium はビルドすると Xcode プロジェクトを生成するので
今回はこれに対してチュートリアル通りに行ってみた。

すると、以下のように AsyncSocket が重複しているというエラーが出る。

ld: duplicate symbol _AsyncSocketException in (以下略)

どうやら Titanium 本体と NativeDriver のために読み込んだ CocoaHTTPServer で
バッティングしている様子。
Xcode/Objective-C 素人なのでどうするのがベストなのかわからないが、
CocoaHTTPServer の方の AsyncSocket.h/AsyncSocket.m をそっと削除したら解決。

main.m でサーバを起動する

Titanium は main.m をビルドごとに自動生成しているため
Xcode 上で main.m を編集してもその内容が破棄されてしまう。
なので、SDK 内にある自動生成元のファイルに対して編集すると良い。
Mac なら以下。
/Library/Application Support/Titanium/mobilesdk/osx/1.7.5/iphone/main.m

Titanium 製アプリを操作する

上記対応をして Xcode からアプリを起動すれば
Android のときと同様に Java のテストコード経由で操作が可能。
ただし要素の取得方法は API が異なり、By.placeholder() など
Android にはない方法も利用できる。

こんな感じ。

// メールアドレスを入力
WebElement mailAddressForm = driver.findElement(By.placeholder("メールアドレス"));
mailAddressForm.sendKeys(mailAddress);
// パスワードを入力
WebElement passwordForm = driver.findElement(By.placeholder("パスワード"));
passwordForm.sendKeys(password);
// ログインボタンをクリック
driver.findElement(By.className(ClassNames.BUTTON)).click();

ということで

Titanium 製 iPhone アプリでも NativeDriver が利用できた!
アプリ側でサーバを起動して、テストコードからそれを叩くという形なので
iPhone/Android でほぼ同じようにテストコードを書けるのが良い。
Titanium で iPhone/Android 両対応すると画面がほぼ同じになるので
テストコードも共通化して書けそうなところが NativeDriver を採用するメリットになりそう。

Titanium 製アプリで NativeDriver を試す(Android 編)

Titanium 製 Android アプリで NativeDriver を試したメモ。
(追記)iPhone
Titanium 製アプリで NativeDriver を試す(iPhone 編) - vaccho's blog

NativeDriver とは

NativeDriver とは、アプリを自動操作してテストが行えるツール。
Web アプリに対して Selenium で行うのと同じ。
というか Selenium に統合された WebDriver API を実装している。
http://code.google.com/p/nativedriver/

自動操作されている様子の動画。

すごい。しかも Android/iOS 両対応。
これをぜひ Titanium 製アプリでもやりたい!

Titanium 製アプリで対応する

チュートリアルにあるように、
1. server-standalone.jar を作成してアプリに取り込む
2. アプリインストール後、コマンドを叩いてサーバを起動する
という手順でアプリ側の準備が整う。
http://code.google.com/p/nativedriver/wiki/AndroidMakeAnAppTestable

jar のインポートはどうやったものかと思ったけど、
以前たまたま見つけた Titanium SDK 内に配置して dependency.json に記載する方法でやってみた。
http://vaccho.hatenablog.com/entry/2011/12/19/125712

すると、インポートしようとはしてくれたけど、以下のエラー。。

[WARN] warning: Ignoring InnerClasses attribute for an anonymous inner class
[ERROR] (org.mortbay.jetty.AbstractBuffers$1) that doesn't come with an
[ERROR] associated EnclosingMethod attribute. This class was probably produced by a
[ERROR] compiler that did not target the modern .class file format. The recommended
[ERROR] solution is to recompile the class from source, using an up-to-date compiler
[ERROR] and without specifying any "-target" type options. The consequence of ignoring
[ERROR] this warning is that reflective operations on this class will incorrectly
[ERROR] indicate that it is *not* an inner class.

どうやら、JDK 1.4 以前でビルドした jar が含まれていると発生するらしい。
エラーの内容を見ると
jetty-6.1.22.jar
jetty-util-6.1.22.jar
が原因のようなので、これを手元でビルドする必要がある。

この辺を参考にビルド。
http://docs.codehaus.org/display/JETTY/Building+from+Source
記載通りにやるとエラーになったので必要なものだけ jar を作るように。
jetty-6.1.22.jar なら以下。

$ svn checkout http://svn.codehaus.org/jetty/jetty/tags/jetty-6.1.22/ jetty-6.1.22
$ cd jetty-6.1.22/modules/jetty
$ mvn install

これを NativeDriver の nativedriver/third_party/jetty 以下に持っていって
改めて server-standalone.jar をビルド。

これを使ったらうまくインポートされた。

Titanium 製アプリを操作する

アプリに jar をインポートして、サーバを起動したら、あとはテストコード経由で操作。
サンプルだとこんな風に記載されている。

driver.startActivity("com.google.android.testing.nativedriver"
        + ".simplelayouts.TextValueActivity");

WebElement textEditView = driver.findElement(By.id("EditText01"));
textEditView.sendKyes("test");

Titanium でも AndroidManifest.xml に Activity の名前は定義されているので
これを使うようにすれば良い。
だけど Titanium だと id が設定できないようで、findElement(By.id()) が使えない。
なので ClassNames を使ってみたらうまくいった。

例えば、こんな画面があったとしたら
f:id:vaccho:20120223161722p:plain

こんな感じでメールアドレスとパスワードを入力して、ログインボタンが押せる。

// 入力欄をすべて取得
List<WebElement> textEditViewList = driver.findElements(AndroidNativeBy.className(ClassNames.EDIT_TEXT));

// 一つ目の入力欄がメールアドレス
WebElement mailAddressEditView = textEditViewList.get(0);
mailAddressEditView.click();
mailAddressEditView.sendKeys(mailAddress);

// 二つ目の入力欄がパスワード
WebElement passwordEditView = textEditViewList.get(1);
passwordEditView.click();
passwordEditView.sendKeys(password);

driver.findElement(AndroidNativeBy.className(ClassNames.BUTTON)).click();

ただ、画面の変更に弱い実装になるので、良い方法を模索したいところ。。

ということで

Titanium 製 Android アプリで NativeDriver が利用できた!
iPhone/Android 両対応だとコードを変更したら iPhone/Android 両方で確認しないとなので
こういう自動化テストはすごく役立つはず。。!

iOS5 SDK で HTTPS 通信

iOS5 SDK でビルドすると HTTPS 通信がステータスコード0でエラーになったので原因のメモ。

いまいち原因がわからなかったが、適当にググってふらふらしてたら
TLS バージョンのデフォルトが iOS4 と iOS5 で異なることがわかった。

For iOS 4, this is effectively TLS_VERSION_1_0. For iOS 5 and greater, this is TLS_VERSION_1_2.

http://developer.appcelerator.com/apidoc/mobile/latest/Titanium.Network.HTTPClient.tlsVersion-property.html

で、Ti.Network.TLS_VERSION_1_0 を明示的に指定してあげたら iOS5 SDK でも大丈夫になった。

var xhr = Ti.Network.createHTTPClient();
xhr.tlsVersion = Ti.Network.TLS_VERSION_1_0;

2012/02/13 追記
tlsVersion プロパティは 1.8.0.1 から。
1.7.x の場合は上記方法が使えない。
https://jira.appcelerator.org/browse/TIMOB-6311

Android でバイナリ送信時のエラーの回避

Android でバイナリ送信時にエラーが発生したのでその回避方法をメモ。
1.8 RC3 で確認。すぐ修正されるかも。

xhr.send({image : blob});

としてバイナリを送信しようとすると
Could not find class 'org.apache.http.entity.mime.content.FileBody'
とエラーが出る。

SDK 内の dependency.json で thirdparty.jar を読み込むようにしてあげたら回避できた。
Mac なら以下。
/Library/Application Support/Titanium/mobilesdk/osx/1.8.0.1.RC3/android/dependency.json

以下のように "android" の先頭に thirdparty.jar を追加。
でもこれが正しい方法なのかわからない。。

"libraries":
{
    "android":["thirdparty.jar","jaxen-1.1.1.jar","ti-commons-codec-1.3.jar","kroll-common.jar","titanium.jar"],
    "xml":["jaxen-1.1.1.jar"],
    "ui":["android-support-v4.jar"]
}

2012/02/13 追記
1.8.1 で修正されたようです。
https://github.com/appcelerator/titanium_mobile/pull/1158

applicationDataDirectory への書き込み

Ti.Filesystem.applicationDataDirectory への書き込みで iPhoneAndroid で違いがあったのでメモ。
Titanium Mobile 1.7.5 で確認。
iPhone では以下のように書くと書き込める。

var f = Ti.Filesystem.getFile(Ti.Filesystem.applicationDataDirectory, fileName);
f.write(blob);

けど、Android だと IOException encountered と出て書き込めない。
以下のようにディレクトリを作成してあげると書き込める。

var directory = Ti.Filesystem.getFile(Ti.Filesystem.applicationDataDirectory, "hoge");
if(!directory.exists()) {
    directory.createDirectory();
}
var f = Ti.Filesystem.getFile(directory.nativePath, fileName);
f.write(blob);

あと、書き込み権限の確認で File オプジェクトのプロパティとして writeable というのが Titanium Studio のオートコンプリートで補完されるけど、writable が正しい。