はじめに
先日ようやくScalaで書いたiOSアプリをApp Storeに登録する事が出来ました。
Tiny AR lite
https://itunes.apple.com/jp/app/tiny-ar-lite/id806062401?mt=8
Scalaで書いたアプリがAppleの審査に通ったのはこれが世界初です。
今回はTiny AR liteを作成して得た経験談を少し書いてみたいと思います。
動画
ニコニコ動画
YouTube
スクリーンショット
環境作成
使用ソフト
- JDK7 http://www.oracle.com/technetwork/jp/java/javase/overview/index.html
- RoboVM http://www.robovm.com/
- sbt-robovm https://github.com/ajhager/sbt-robovm
コンパイルにXcode5が必須のため、OSは必然的にMacOSX 10.9.1となります。
今回、すべての設定を行ったサンプルプロジェクトを用意しました。
https://github.com/chototsu/mikumikustudio-gdx
UI作成
UIを作成する方法は3種類あります。
- niftyguiを使用する。
- JavaFXを使用する。
- OSネイティブなUIライブラリを使用する。
1のniftyguiはOpenGLを使ったUIライブラリです。ゲームのコンソール等に向いています。しかし日本語が不便(使えない事はない)なのと、タッチパネルのジェスチャー対応がイマイチなのが難点です。
2のJavaFXはPC,Android,iOSで動作します。将来有望な方法と思われます。
3のOSネイティブなUIライブラリは、対応OSごとにコードを書く必要があります。Android,iOS,PCをサポートする場合は3つも書かねばならず大変です。しかしOSとの親和性は一番です。
今回は3の方法を使用しました。
##2014/2/23 追記:
xcodeとの連携を自動化しました。
http://qiita.com/chototsu/items/b2b9196434a3eaead614
OS依存コードと共通コードの切り分け
共通コードはcommonサブプロジェクトに、OS依存コードはそれぞれios,android,desktopサブプロジェクトに書きます。
機種依存コードから共通コードを呼び出す
共通コードから機種依存コードを呼び出すためのインターフェースをcommonプロジェクトに作成します。
今回はこのようなインターフェースを作成しました。
public interface TinyARAPI {
public void selectItemFromList(int x, int y, int width, int height, String title, List<String> model, ListItemSelectedCallback callback);
public void showIndicator(String msg);
public void stopIndicator();
public void setShowMainPanel(boolean flag);
}
OS依存コードに上記インターフェースを実装したクラスを作成します。
例えばiOSだと以下のようになります。
class Main extends IOSApplication.Delegate with TinyARAPI{
override protected def createApplication(): IOSApplication = {
val config = new IOSApplicationConfiguration()
app = new IOSApplication(new MyGame(this), config)
app
}
}
OS依存コードから共通コードを呼び出す
機種依存コードから共通コードへのアクセスは普通にpublicメソッドを呼び出すだけで出来ます。
スレッドに関する注意
Android, desktopではOpenGLのスレッドとOS提供GUIのスレッドが違います。OpenGLの命令を実行する場合はOpenGLのスレッドで、OSネイティブなGUIコンポーネントにアクセスする場合はGUIのスレッドで実行する必要があります。
GUIスレッドからOpenGLスレッドを呼び出す
Application#enqueueメソッドでCallableを渡します。するとOpenGLのスレッドでCallableが実行されます。
testApp.enqueue(new Callable[Void] {
def call(): Void = {
testApp.asInstanceOf[TestApp].loadMotion(path)
null
}
})
OpenGLスレッドからGUIスレッドを呼び出す
Androidの場合はHandlerを、Swingの場合はSwingUtilities#invokeLaterを使用します。
iOSはGUIスレッドとOpenGLスレッドが同一ですのでそのまま呼んでも大丈夫です。
XCodeとの連携
RoboVMにはCocoaTouchバインディングが標準で実装されており、Xcodeを使わなくてもだいたいの事は出来ます。しかしフォームの作成などはXCodeを使った方が効率的です。そこで今回はsbtのプロジェクトとは別にXCodeのプロジェクトを1個作成しました。
プロジェクトの作成
プロジェクトは一番簡単なSingle View Applicationで作成するのが良いでしょう。
作成したらライブラリ作成用のターゲットを作成します。ここでmmslibという生で作成する事にします。
フォームの追加
Xcode5は標準でstoryboardを使用しますがRoboVMから使用する場合はxibを使う方が良いです。
new fileでUIViewControllerのサブクラスを作成します。この際、With XIB for user interfaceにチェックを付けるのを忘れないで下さい。
クラスが出来たら普通にフォームを作成します。
xibをnibにコンパイルする。
コンパイルはコマンドラインで行うのが簡単です。
xcodebuild -sdk iphonesimulator7.0 -arch i386 clean build
こうすると/build/Release-iphonesimulator/xxx.app/ の下にnibファイルが出来ますのでそれらをios/src/main/resources/の下にコピーします。
Java側のクラスを作成する
こちらを参照して下さい。
http://qiita.com/chototsu/items/a289cd49f11345969868
Objective Cのコードをリンクする
Xcodeのプロジェクトにクラスを追加します。この際ターゲットにmmslibも含めておきます。
コンパイルは以下のコマンドで行います。
xcodebuild -sdk iphonesimulator7.0 -arch i386 -target mmsslib clean build
xcodebuild -sdk iphoneos7.0 -arch armv7 -arch armv7s -target mmslib clean build
xcrun lipo -create build/Release-iphonesimulator/libmmslib.a build/Release-iphoneos/libmmslib.a -output ../mikumikustudio-gdx/ios/lib/libmmslib.a
このコマンドでシミュレータと実機で動作するライブラリが作成され、プロジェクトに追加されます。
呼び出し方はこちらを参照して下さい。
http://qiita.com/chototsu/items/f64a866f0d78449e5c92
RoboVMのCocoa Touchバインディングに足りないメソッドがあったら
RoboVMのCocoa Touchバインティングは完全ではなく、いくつか足りないメソッドがあったりします。例えば今回のプロジェクトではNSURLRequest#requestFromURLが無いという問題がありました。そういう場合は自分で作成するのが良いです。今回はこのようなメソッドを作成しました。
private static final Selector requestWithURL = Selector.register("requestWithURL:");
@Bridge
public native static NSURLRequest objc_requestWithURL(ObjCClass clazz, Selector selector, NSURL url);
ここで注目するのは、objc_requestWithURLがstaticであるという事です。そのためこのメソッドはどのクラスに実装しても構いません。最初いちいちRoboVMのライブラリを改変しないといけないのかと思いましたがこの事に気付いてからはサクサク作業が進みました。
GCの問題
Objective CとJavaのコードを共存させた場合、GCの問題に注意する必要があります。
Javaのオブジェクトはすべての参照が無くなった時点でGCの対象になります。
これに対しObjective Cのオブジェクトは参照カウンタが0になった時点で削除されます。
そのため以下の2つのパターンが発生します。
- JavaのオブジェクトがGCされ、Objective Cのオブジェクトが生存している。
- Javaのオブジェクトが生存しており、Objective Cのオブジェクトが消えている。
1のパターンでは、再びJavaのオブジェクトがアクセスされた時にJavaのオブジェクトが再生成されます。このためJavaのオブジェクトで持っていたデータはすべて初期化されます。
2のパターンでは、Objective Cのオブジェクトにアクセスした際にアプリが落ちます。
これらを回避するためアプリでは以下のような対策が必要です。
- JavaのオブジェクトはJava側の責任でGCされないよう参照を保持する。
- delegateなど弱参照になるObjective CのオブジェクトはJava側でaddStrongRef()で参照を保持する。
通常、Java側で作成したUIコンポーネントなどをJava側のインスタンス変数に持たせる程度の対応で特に問題は発生しません。
問題が起きるのはdelegeteやUINavigationControllerなどです。
delegateは弱参照ですのでJava側で忘れずaddStrongRef()してやる必要があります。
UINavigationControllerはちょっと厄介です。というのもオブジェクトが消えるタイミングがユーザーのアクションがあった時だからです。
色々考えた末、UINavigationControllerのサブクラスを作る事でこれを回避しました。
@CustomClass("MyNavigationController")
class MyNavigationController(vc : UIViewController) extends UINavigationController(vc) {
var list = new util.ArrayList[UIViewController]()
override def pushViewController(viewController: UIViewController, animated: Boolean): Unit = {
if (list == null) {
list = new util.ArrayList[UIViewController]()
}
list.add(viewController)
addStrongRef(viewController)
System.err.println("viewController size = "+list.size())
super.pushViewController(viewController, animated)
}
override def popViewControllerAnimated(animated: Boolean): UIViewController = {
val vc = super.popViewControllerAnimated(animated)
System.err.println("popViewControllerAnimated------------")
list.remove(vc)
removeStrongRef(vc)
vc
}
override def popToRootViewController(animated: Boolean): NSArray[_ <: NSObject] = {
val array = super.popToRootViewController(animated)
for(i <- 0 until array.size()) {
val vc = array.get(i)
list.remove(vc)
removeStrongRef(vc)
}
array
}
override def popToViewController(viewController: UIViewController, animated: Boolean): NSArray[_ <: NSObject] = {
val array = super.popToViewController(viewController, animated)
for(i <- 0 until array.size()) {
val vc = array.get(i)
list.remove(vc)
removeStrongRef(vc)
}
array
}
}
このクラスではviewのpush, popのタイミングで参照の追加・解放を行っています。
コンストラクタの問題
Objective CからJava側のオブジェクトが自動生成された場合、Javaのコンストラクタは呼ばれません。
例えば以下のコードを書いた場合、hogeがnullになります。
class xxx extends IOSApplication.Delegate{
final Hoge hoge = new Hoge();
}
対策として、createApplication()などの初期化メソッドで初期化するようにします。
証明書とprovisioning profileの問題
証明書とprovisioning profileの問題は非常にややこしいです。私もまだ完全には理解していません。
実機で動かない場合
Xcodeから何かプログラムを実機で動かします。すると証明書等が転送されるみたいで、以後、RoboVMからも実機で実行できるようになります。
provisioning profileの問題
通常はRoboVMが適当なprovisioning profileを検索し使用してくれます。しかし複数のprovisioning profileが存在する場合、間違う事があるようです。その場合は明示的に指定するのが最も簡単な方法です。
PCにインストールされているprovisioning profileは~/Library/MobileDevice/Provisioning Profilesの下にあります。この中から適当なprovisioning profileを探し、ファイル名を控えます。そのファイル名から拡張子を除いた番号をios/build.sbtに記述します。
iosProvisioningProfile := Option("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
アイコン
iOS用のアイコンはios/src/main/resourcesの下に置きます。
アイコンを作ったらinfo.plistに追加するのを忘れないで下さい。
メモリーの問題
1GBから最近は2GB積んでいる物が多いAndroidに比べiOS端末はメモリーが少ないです。そのためそのままAndroidから移植するとメモリー不足で落とされまくるという羽目になります。
有効な対策は少ないのですが、メモリーを使う前、使った後に明示的にSystem.gc()を実行すると多少はマシになります。
RoboVMはSystem.gc()を実行するとヒープを縮小し、OSに返してくれます。
TODO
- 現在Xcodeとの連携は手作業で行っていますがこれを自動化したいです。
- xibからJava側のクラスを生成するのは現在手作業で行っていますがnibはxmlですので自動化が出来そうです。これを自動化したいです。
終わりに
1本アプリを作ってみて、RoboVMが十分実用レベルにあるという事が確認出来ました。
またサポートも非常に良く、MLで質問すると即座に返事が返ってきます。
https://groups.google.com/forum/#!forum/robovm
皆様もぜひ使ってみてください。
私の記事がRoboVMの普及の助けになれば幸いです。