Edited at
ClojureDay 11

ClojureはiOSの夢を見るか

More than 5 years have passed since last update.

 にゃんぱすー。これは、Clojure Advent Calendar 2013の11日目の記事です。Clojure で iOS アプリがつくれるらしい、という話をします。

 早い話が、Java で iOS ネイティブアプリをつくれるという RoboVM と、そのヘルパであるプラグイン lein-fruit の紹介です。


RoboVM

 Java のバイトコードを ARM/x86 のネイティブコードに変換してくれる代物です。Java と Cocoa API の橋渡しをするクラス群も含まれているので、あたかも Cocoa API を直接扱っているかのようにコードを書くことができます。公式から引用すると、こんな感じ。

import org.robovm.cocoatouch.coregraphics.*;

import org.robovm.cocoatouch.foundation.*;
import org.robovm.cocoatouch.uikit.*;

public class IOSDemo extends UIApplicationDelegate.Adapter {

private UIWindow window = null;
private int clickCount = 0;

@Override
public boolean didFinishLaunching(UIApplication application, NSDictionary launchOptions) {

final UIButton button = UIButton.fromType(UIButtonType.RoundedRect);
button.setFrame(new CGRect(115.0f, 121.0f, 91.0f, 37.0f));
button.setTitle("Click me!", UIControlState.Normal);

button.addOnTouchUpInsideListener(new UIControl.OnTouchUpInsideListener() {
@Override
public void onTouchUpInside(UIControl control, UIEvent event) {
button.setTitle("Click #" + (++clickCount), UIControlState.Normal);
}
});

window = new UIWindow(UIScreen.getMainScreen().getBounds());
window.setBackgroundColor(UIColor.lightGrayColor());
window.addSubview(button);
window.makeKeyAndVisible();

return true;
}

public static void main(String[] args) {
NSAutoreleasePool pool = new NSAutoreleasePool();
UIApplication.main(args, null, IOSDemo.class);
pool.drain();
}
}


lein-fruit

 RoboVM でのビルドをサポートする Leiningen のプラグインです。RoboVM 公式というわけではありません。


インストール

 まず、最新の RoboVM をダウンロードして適当な場所に解凍してください。わかりづらいんですけど、ここの Download the latest RoboVM release のところ。

 次に、profiles.clj を編集します。~/.lein/profiles.clj に以下を追記してください。

{:user {

:plugins [[lein-fruit "0.1.1"]]
:ios {:robovm-path "/absolute/path/to/robovm-0.0.7"}
}}

 主な使い方は、

# プロジェクトを生成する

$ lein fruit new fujiyama

# ビルドしてシミュレータで実行する
$ lein fruit doall

# アプリを生成して実機で実行する
$ lein fruit release


実行

 プロジェクト生成のときに Hello world 的なコードが追加されるので、そのまま実行してみると良いと思います。ビルドに初回で8分くらい、二回目以降で2分くらい待ったかも(いずれも、Macbook Pro Early 2011)。結構時間がかかります。

 追加された src/clojure/fujiyama/core.clj を見てみると、こんな感じに。

(ns fujiyama.core

(:require [fujiyama.core-utils :as u]))

(def window (atom nil))

(defn init
[]
(let [main-screen (u/static-method :uikit.UIScreen :getMainScreen)
button-type (u/static-field :uikit.UIButtonType :RoundedRect)
button (u/static-method :uikit.UIButton :fromType button-type)
normal-state (u/static-field :uikit.UIControlState :Normal)
click-count (atom 0)]
(doto button
(.setFrame (u/init-class :coregraphics.CGRect 115 121 91 37))
(.setTitle "Click me!" normal-state)
(.addOnTouchUpInsideListener
(proxy [org.robovm.cocoatouch.uikit.UIControl$OnTouchUpInsideListener] []
(onTouchUpInside [control event]
(.setTitle button (str "Click #" (swap! click-count inc)) normal-state)))))
(reset! window (u/init-class :uikit.UIWindow (.getBounds main-screen)))
(doto @window
(.setBackgroundColor (u/static-method :uikit.UIColor :lightGrayColor))
(.addSubview button)
.makeKeyAndVisible)))

インスタンスメソッドは普通に呼び出せるけれど、クラスメソッドは fujiyama.core-utils/static-method を使わなくてはならないようです。この関数は何ですか?

(defn- types-match?

[types args]
(and (= (count types) (count args))
(->> (for [i (range (count types))]
(let [parent (get types i)
child (type (get args i))]
(or (nil? child) (.isAssignableFrom parent child))))
(filter true?)
count
(= (count types)))))

(defn- get-object
[objects args]
(-> #(types-match? (.getParameterTypes %) args)
(filter objects)
first))

(defn- get-obj-method
[class-name method-name args]
(-> (->> (Class/forName class-name)
.getMethods
(filter #(= method-name (.getName %))))
(get-object args)))

(defn- parse-name
[class-name]
(if (keyword? class-name)
(str "org.robovm.cocoatouch." (name class-name))
class-name))

(defn static-method
[class-name method-name & args]
(let [class-name (parse-name class-name)
method-name (name method-name)
method (get-obj-method class-name method-name args)]
(if method
(.invoke method nil (into-array Object args))
(println "Couldn't find class/method:" class-name method-name))))

どうやら、リフレクションでメソッドを取得して呼び出しているようです。


その他

 ずっと前に Clojure から素の RoboVM に触ってみようとしたことがあるんですけど、そのときは例外を吐いて死んでしまい上手くビルドができませんでした。現在も解決していないようで、lein-fruit ではそれを回避するために Java のプログラムとしてビルドして、そこから Clojure のコードを呼んでいる、のかな、多分。その辺りの経緯は以下を。


Clojure + RoboVM issue - Google Group

lein-fruit: a Leiningen plugin for RoboVM - Google Group



終わりに

 RoboVM と leinf-fruit どちらも発展途上ではありますが、RoboVM は活発に開発が成されていますし、lein-fruit は……ま、まあいずれ RubyMotion や mocl に追従する開発環境となると良いですね。ということでした。


RoboVM

oakes/lein-fruit - GitHub