Clojure
ClojureScript
iOS
OM
React
ClojureDay 20

ClojureScriptとReact NativeによるiOSアプリ開発

More than 1 year has passed since last update.

はじめに

iOSアプリ開発の主流は,Objective-CやSwiftを用いたネイティブアプリ開発です.HTML5を用いたCordovaや,UnityからiOSエクスポートするといった方法もあります.そのような中,Facebookの発表したReact Nativeでは,Virtual DOMで大きな注目を浴びたReact.jsと同様のスタイルでJavaScriptを記述することで,iOSアプリ開発を行うことができます.

一方で,Clojureの文法で記述したコードをJavaScriptにコンパイルするClojureScriptの世界では,Omと呼ばれるReact.jsラッパーが人気です.React.jsの仕組みはイミュータブルであることを根幹においているClojureと非常に相性が良いといわれています.

React.jsのような仕組みを使えるReact Nativeと,React.jsのラッパーであるOm,これらはうまく組み合わせることができます.本稿では,ClojureScriptとReact Nativeにより,iOSアプリ開発を行う方法を紹介していきます.

アプローチ

cljs-react-native.jpg

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は,ClojureScriptとReact Nativeを用いたプロジェクトの作成から実行まで,数種のコマンドで行うことを可能とするビルドツールです.本稿ではこのNatalを用いて,iOSアプリ開発を行う流れを見ていきます.

使い方

準備

Natalはnpmでインストールすることができます.

$ npm install -g natal

Natalの内部では様々なコマンドが使用されるため,以下のものをあらかじめインストールしておく必要があります.

プロジェクトの作成

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になります.

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というReact Native APIの薄いラッパーを通して,React Nativeの機能を利用しています.ここではOmについて説明しませんが,Omを使ったことのある人ならば,非常に理解しやすいコードになっていると思います.

実行

natal launchコマンドで,iOSシミュレータ上でアプリケーションを実行できます.

$ natal launch

あるいは,natal xcodeコマンドでXcodeが立ち上がるため,Xcode上でRunして実行しても構いません.

default-app.png

そのまま実行すると “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")

natal-repl.gif

Objective-CやSwiftのようにリビルドする必要はなく,通常のReact Nativeのようにリロードする必要すらありません.REPLで行った変更は即座に反映され,シミュレータ上の文字列が変わっていきます.

実践

それでは,OmのBasic Tutorialの一部にある,削除機能を備えたコンタクトリストを実装してみます.

core.asyncを用いるので,project.cljのdependenciesに加えます.

project.clj
: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します.imagealertは使わないので消します.

core.cljs
(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-namemiddle-nameを定義します.このあたりはOmのBasic Tutorialと完全に一緒です.

core.cljs
(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に設定しておきます.

core.cljs
(om/root contacts-view app-state {:target 1})

contacts-view, contact-viewは次のようにします.

core.cljs
(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}}))))))

viewtextといったUIコンポーネントを設定するところ以外は,OmのBasic Tutorialと一緒です.つまり,これまでOmを使って開発してきた経験を活かしてiOSアプリを作れるということがわかります.

実行すると,名前の一覧とそれぞれ右側に “Delete” ボタンが配置されているはずです.“Delete” ボタンを押すと,該当する名前が消えるのがわかります.

contact-list.gif

なお,今回は簡略化のため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を用いることも可能です1.プロジェクト作成時にnatal init ExampleApp --interface om-nextとすることで,Om Nextを用いたひな形が生成されます.興味がある方は,ぜひそちらも試してみてください.

今年のClojure/conj 2015において,Jearvon Dharrieさんが同様のテーマで講演されています2YouTubeに動画があるので,こちらも見てみると面白いと思います.