はじめに
iOSアプリ開発の主流は,Objective-CやSwiftを用いたネイティブアプリ開発です.HTML5を用いたCordovaや,UnityからiOSエクスポートするといった方法もあります.そのような中,Facebookの発表した[React Native][react-native]では,Virtual DOMで大きな注目を浴びたReact.jsと同様のスタイルでJavaScriptを記述することで,iOSアプリ開発を行うことができます.
一方で,Clojureの文法で記述したコードをJavaScriptにコンパイルするClojureScriptの世界では,[Om][om]と呼ばれるReact.jsラッパーが人気です.React.jsの仕組みはイミュータブルであることを根幹においているClojureと非常に相性が良いといわれています.
React.jsのような仕組みを使えるReact Nativeと,React.jsのラッパーであるOm,これらはうまく組み合わせることができます.本稿では,ClojureScriptとReact Nativeにより,iOSアプリ開発を行う方法を紹介していきます.
アプローチ
React NativeはJavaScriptのランタイムをもっており.JavaScriptで書かれたコードをiOSアプリにバンドルし,JavaScriptランタイムで動作させます.JavaScriptとiOSネイティブSDKへのブリッジを用いることで,UIKit等のネイティブコンポーネントを使うことができます.JavaScriptのコードはReact.jsのスタイルで記述できるため,一度学習すれば様々なプラットフォームで活かせるという長所があります.
ClojureScriptは,Clojureで書かれたコードをJavaScriptにコンパイルします.シンプルなClojure文法で記述できるというだけでなく,サーバ側とクライアント側(Webブラウザ)でコードを共有できるという利点があります.ClojureScriptライブラリのひとつであるOmは,React.jsのラッパーです.Omを使うことで,ClojureScriptにおいても自然な形でReact.jsの機能を利用して,クライアント側のコードを記述できます.
Omを利用したClojureScriptのコードをReactスタイルのJavaScriptにコンパイルし(①),そのJavaScriptをiOSアプリにバンドル(②),React NativeのJavaScriptランタイムで動作させる(③),という流れで開発すれば,ClojureScriptでiOSアプリ開発が可能となります.①〜③の流れを簡単に実行できるようにしてくれるツールが[Natal][natal]です.Natalは,ClojureScriptとReact Nativeを用いたプロジェクトの作成から実行まで,数種のコマンドで行うことを可能とするビルドツールです.本稿ではこのNatalを用いて,iOSアプリ開発を行う流れを見ていきます.
使い方
準備
Natalはnpmでインストールすることができます.
$ npm install -g natal
Natalの内部では様々なコマンドが使用されるため,以下のものをあらかじめインストールしておく必要があります.
-
npm
>=1.4
-
Node.js
>=4.0.0
-
Node.js
-
Leiningen
>=2.5.3
-
CocoaPods
>=0.38.2
-
Ruby
>=2.0.0
-
Ruby
-
Xcode (+ Command Line Tools)
>=6.3
-
OS X
>=10.10
-
OS X
-
Watchman
>=3.7.0
-
rlwrap
>=0.42
-
react-native-cli
>=0.1.7
(npm install -g react-native-cli
)
プロジェクトの作成
natal init
コマンドで,プロジェクトのひな形を作成します.
$ natal init ExampleApp
作成されるプロジェクトのディレクトリ構造は以下のようになります.
example-app/
├── CHANGELOG.md
├── LICENSE
├── README.md
├── dev-resources/
├── doc/
│ └── intro.md
├── native/
│ ├── index.ios.js
│ ├── ios/
│ │ ├── Podfile
│ │ ├── Podfile.lock
│ │ ├── Pods/
│ │ ├── ExampleApp/
│ │ ├── ExampleApp.xcodeproj
│ │ ├── ExampleApp.xcworkspace
│ │ └── ExampleAppTests/
│ ├── node_modules/
│ └── package.json
├── project.clj
├── resources/
├── src/
│ └── example_app/
│ └── core.cljs
└── test/
└── example_app/
└── core_test.clj
大枠はClojureのビルドツールであるLeiningenのプロジェクト構成に沿っています.ファイル数が多いため一部省略していますが,native/
以下が通常のReact Nativeのプロジェクトです.主に編集していくのは,src/example_app/core.cljs
になります.
(ns example-app.core
(:require-macros [natal-shell.core :refer [with-error-view]]
[natal-shell.components :refer [view text image touchable-highlight]]
[natal-shell.alert-ios :refer [alert]])
(:require [om.core :as om]))
(set! js/React (js/require "react-native/Libraries/react-native/react-native.js"))
(defonce app-state (atom {:text "Welcome to ExampleApp"}))
(defn widget [data owner]
(reify
om/IRender
(render [this]
(with-error-view
(view
{:style
{:flexDirection "column" :margin 40 :alignItems "center"}}
(text
{:style
{:fontSize 50 :fontWeight "100" :marginBottom 20 :textAlign "center"}}
(:text data))
(image
{:source
{:uri "https://raw.githubusercontent.com/cljsinfo/logo.cljs/master/cljs.png"}
:style {:width 80 :height 80 :marginBottom 30}})
(touchable-highlight
{:style {:backgroundColor "#999" :padding 10 :borderRadius 5}
:onPress #(alert "HELLO!")}
(text
{:style {:color "white" :textAlign "center" :fontWeight "bold"}}
"press me")))))))
(om/root widget app-state {:target 1})
core.cljs
の中身は,シンプルなOmのコードになっています.[natal-shell][natal-shell]というReact Native APIの薄いラッパーを通して,React Nativeの機能を利用しています.ここではOmについて説明しませんが,Omを使ったことのある人ならば,非常に理解しやすいコードになっていると思います.
実行
natal launch
コマンドで,iOSシミュレータ上でアプリケーションを実行できます.
$ natal launch
あるいは,natal xcode
コマンドでXcodeが立ち上がるため,Xcode上でRun
して実行しても構いません.
そのまま実行すると “Welcome to ExampleApp” という文字列,ClojureScriptのロゴ,そして “press me” と書かれたボタンが配置されているはずです.“press me” ボタンをタップすると,“HELLO!” というアラートが表示されます.
ちなみに,natal launch
で立ち上がるiOSシミュレータの種類は,natal setdevice
で変更することができます.natal listdevices
で,使用可能なシミュレータの一覧が表示されるので,その中から適当なものを選んでください.
REPL
Clojure/ClojureScriptの強みとして,REPLを用いた動的な開発があげられます.Natalでも,もちろんREPLを使用することができます.以下のコマンドにより,REPLを立ち上げましょう.
$ rlwrap natal repl
“Welcome to ExampleApp” という文字列は,core.cljs
に(defonce app-state (atom {:text "Welcome to ExampleApp"}))
と定義されているので,これを動的に変更してみます.
cljs.user=> (in-ns 'example-app.core)
example-app.core=> (swap! app-state assoc :text "Hello Native World")
Objective-CやSwiftのようにリビルドする必要はなく,通常のReact Nativeのようにリロードする必要すらありません.REPLで行った変更は即座に反映され,シミュレータ上の文字列が変わっていきます.
実践
それでは,Omの[Basic Tutorial][om-basic-tutorial]の一部にある,削除機能を備えたコンタクトリストを実装してみます.
[core.async][core-async]を用いるので,project.clj
のdependenciesに加えます.
:dependencies [[org.clojure/clojure "1.7.0"]
[org.clojure/clojurescript "1.7.170"]
[org.omcljs/om "0.9.0"]
[org.omcljs/ambly "0.6.0"]
[natal-shell "0.1.2"]
[org.clojure/core.async "0.2.374"]] ; <- 追加
core.cljs
を編集していきます.まずcore.asyncをrequire
します.image
とalert
は使わないので消します.
(ns example-app.core
(:require-macros [natal-shell.core :refer [with-error-view]]
[natal-shell.components :refer [view text touchable-highlight]]
[cljs.core.async.macros :refer [go]])
(:require [om.core :as om :include-macros true]
[cljs.core.async :refer [put! chan <!]]))
app-state
にデータであるコンタクトリストを入れます.また,画面に表示する文字列を生成するための関数display-name
とmiddle-name
を定義します.このあたりはOmのBasic Tutorialと完全に一緒です.
(defonce app-state
(atom
{:contacts
[{:first "Ben" :last "Bitdiddle" :email "benb@mit.edu"}
{:first "Alyssa" :middle-initial "P" :last "Hacker" :email "aphacker@mit.edu"}
{:first "Eva" :middle "Lu" :last "Ator" :email "eval@mit.edu"}
{:first "Louis" :last "Reasoner" :email "prolog@mit.edu"}
{:first "Cy" :middle-initial "D" :last "Effect" :email "bugs@mit.edu"}
{:first "Lem" :middle-initial "E" :last "Tweakit" :email "morebugs@mit.edu"}]}))
(defn middle-name [{:keys [middle middle-initial]}]
(cond
middle (str " " middle)
middle-initial (str " " middle-initial ".")))
(defn display-name [{:keys [first last] :as contact}]
(str last ", " first (middle-name contact)))
デフォルトのcore.cljs
に存在するwidget
関数はもう使わないので,消してしまいましょう.そして,新たにcontacts-view
という関数を追加し,om/root
に設定しておきます.
(om/root contacts-view app-state {:target 1})
contacts-view
, contact-view
は次のようにします.
(defn contact-view [contact owner]
(reify
om/IRenderState
(render-state [this {:keys [delete]}]
(view
{:style
{:flexDirection "row" :margin 20 :alignItems "center"}}
(text
{:style
{:fontSize 17 :fontWeight "100" :marginBottom 20 :textAlign "left"}}
(display-name contact))
(touchable-highlight
{:style {:backgroundColor "#999" :marginLeft 10 :padding 10 :borderRadius 5}
:onPress (fn [e] (put! delete @contact))}
(text
{:style {:color "white" :textAlign "center" :fontWeight "bold"}}
"Delete"))))))
(defn contacts-view [data owner]
(reify
om/IInitState
(init-state [_]
{:delete (chan)})
om/IWillMount
(will-mount [_]
(let [delete (om/get-state owner :delete)]
(go (loop []
(let [contact (<! delete)]
(om/transact! data :contacts
(fn [xs] (vec (remove #(= contact %) xs))))
(recur))))))
om/IRenderState
(render-state [this {:keys [delete]}]
(with-error-view
(view
{:style
{:flexDirection "column" :margin 40 :alignItems "center"}}
(text
{:style
{:fontSize 50 :fontWeight "100" :marginBottom 20 :textAlign "center"}}
"Contact list")
(om/build-all contact-view (:contacts data)
{:init-state {:delete delete}}))))))
view
やtext
といったUIコンポーネントを設定するところ以外は,OmのBasic Tutorialと一緒です.つまり,これまでOmを使って開発してきた経験を活かしてiOSアプリを作れるということがわかります.
実行すると,名前の一覧とそれぞれ右側に “Delete” ボタンが配置されているはずです.“Delete” ボタンを押すと,該当する名前が消えるのがわかります.
なお,今回は簡略化のためview
の中に要素をそのまま入れていますが,iOSアプリとしてはUITableView
にあたるlist-view
(React NativeのListView
)を用いて実装したほうが良いと思います.
おわりに
ClojureScriptとReact Nativeを用いてiOSアプリ開発を行う手法を紹介しました.この手法ではiOSアプリ開発において,Clojure/ClojureScriptの強力なツールであるREPLやOmを活かして,動的な開発かつシンプルなコードを実現可能とします.Clojure/ClojureScriptによって,サーバ,Webブラウザ,モバイルという3つの環境において共通のコードを利用することができるかもしれません.
本稿では現行のOmを利用しましたが,@snufkonさんが紹介されていた[Om Next][om-next]を用いることも可能です1.プロジェクト作成時にnatal init ExampleApp --interface om-next
とすることで,Om Nextを用いたひな形が生成されます.興味がある方は,ぜひそちらも試してみてください.
今年のClojure/conj 2015において,Jearvon Dharrieさんが同様のテーマで講演されています2.[YouTubeに動画][mobile-apps-cljs]があるので,こちらも見てみると面白いと思います.
-
Mobile Apps with ClojureScript - Jearvon Dharrie
[react-native]: https://facebook.github.io/react-native/
[om]: https://github.com/omcljs/om
[om-basic-tutorial]: https://github.com/omcljs/om/wiki/Basic-Tutorial#your-first-om-component
[natal]: https://github.com/dmotz/natal
[natal-shell]: https://github.com/dmotz/natal-shell
[om-next]: https://github.com/omcljs/om/wiki/Quick-Start-(om.next)
[core-async]: https://github.com/clojure/core.async
[mobile-apps-cljs]: https://www.youtube.com/watch?v=GDA-g6Ca_dQ ↩