Monaca x CordovaxONSEN UIでハイブリッドアプリを検証

# Monaca x Cordova x Angular x OnsenUI

## 今回作るもの

Cordovaを利用したハイブリッドアプリを勉強がてらリリースしたため、書きためる。
[ワイフードサーチ](https://play.google.com/store/apps/details?id=com.wife.food&hl=ja)というハイブリッドアプリをリリースしました。
※社内の勉強会展開のため、バグだらけ&&作り途中で出しちゃいましたが…

[ソース](https://github.com/yueguchi/wifood)はコチラ。これからハイブリッドを作ろうという人の参考(になるかはわからんけど)になれば。

![alt](https://lh3.googleusercontent.com/KSZiLE1RbIkH9RO58eokmekkTcyNSG30fVWhs8Jdy4nUs9e1kd2-BTLNIPPfAglR=h900)

### アプリの概要
ぐるなびAPIを利用し、ハイブリッドアプリでどこまでネイティブに近づけてアプリが作れるかを検証。
ハイブリッドアプリ作成にあたり、Cordovaを。
UI、およびフロント開発技術としてONSEN UIとAngularJSを利用しています。
基本的な動線は

* エリアから探す
* フリーワードから探す
* 現在地から探す(地図検索) & お店までのルート案内

となってます。

![alt](https://lh3.googleusercontent.com/yKJkAUwmwId4BD1Z3TBMWWytuDEIV86me9AOw0HzDi0NJ3P5ciVOWNXH_V0NT6Wh_A=h900)

## [Cordova](https://cordova.apache.org)とは

![alt](https://cordova.apache.org/images/cordova_bot.png)

旧Adobe社PhoneGap。今はapacheに寄贈し、**<font color="red">Cordova</font>**と呼ばれるようになった。

かいつまんでいうと、AndroidやiOSのWebView(UIWebView)の機能を利用し、JavaScript経由でネイティブアプリのAPIをコールして利用する仕組みを提供するフレームワーク。

**<font color="red">HTML5 x CSS3 x JavaScriptを開発言語としてネイティブアプリ、しかもAndroidやiOS、Windowsアプリまでビルド可能なハイブリッドアプリの作成を可能とする技術</font>**

## [ONSEN UI](http://ja.onsen.io)とは

![alt](http://ja.onsen.io/images/header/onsen_logo.png)

Asial社が提供するスマホネイティブライクなUIを提供するUIライブラリ。
AngularJSで作成されており、当然AngularJSとの親和性が高い。
ハイブリッドアプリは二三年前から話題には上がっていたが、UIはあくまで開発者が独自に作り込むものだった。
しかし、ONSEN UIはスマホライクなUIをかなり忠実に提供してくれるため、開発者はONSEN UIのコンポーネントを目的に応じて、選択することが可能となる。
いわゆるスライディングメニューやナビゲーション等、スマホマストアイテムも揃っており、テーマも柔軟に選択が可能。
SPA(シングルページアプリケーション)の作成に特化したライブラリとなっている。

# [Angular JS](https://angularjs.org)

![alt](https://angularjs.org/img/AngularJS-large.png)

言わずもがな。Google社製のMVCフレームワークである。

# ソースベースでの紹介

今回はPJの基盤は[Monaca](https://ja.monaca.io)を使用して作成したため、
ナビゲーション付きUIをもとに雛形を作成してから開発に着手した。
CordovaのpluginやONSEN UIのテーマは後から追加・変更した形となる。

## PJの作成

今回はONSEN UIのテンプレートからスタートし、それにSliding-menuを付け足して基盤としました。

１. Monacaのダッシュボードから「開発をスタート」を選択し、「Monaca.ioで開発」を選択する

![start.png](https://qiita-image-store.s3.amazonaws.com/0/78857/bef8c34a-a41f-e2ea-8df1-9ddd6e668e9e.png)

２. テンプレートから「Onsen UI」を選択する

![select.png](https://qiita-image-store.s3.amazonaws.com/0/78857/2c1c6694-ccb0-db3e-d100-0e0c29f4d85a.png)

３. tabbarを選択する

![tabbar.png](https://qiita-image-store.s3.amazonaws.com/0/78857/27a0febc-3fb2-bd0d-2a89-51244e8d875e.png)


PJの作成は以上です。これでPJ名と説明をよしなに埋めてくれれば、足場が自動で作成されます。

なお、ソースディレクトリ構成は以下となる(github見てくれればわかるけど)

![src.png](https://qiita-image-store.s3.amazonaws.com/0/78857/42efaf9d-d6a1-bc38-26b8-1f2285b5256d.png)

「www」ディレクトリがCordovaPJの特徴となる。
androidやiOS、chromeは、リリースビルド対象のプラットフォーム設定ファイル。

## index.htmlについて

Cordovaプロジェクトを作成すると、入り口はこのindex.htmlとなる。
SPAである以上、画面遷移はこのindex.htmlに対してDOMをAjax的に差し替えて**「画面遷移のように見せる」挙動**をとっていくことになる。

今回は冒頭に貼り付けた画像を見てわかる通り、「ons-tabbar」と「ons-slidingmenu」を組み合わせたUIを基調としているため、以下のようなコードとなっている。

```
<!DOCTYPE HTML>
<html ng-app="myApp">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    <script src="components/loader.js"></script>
    <script type="text/javascript" src="https://maps.googleapis.com/maps/api/js"></script>
    <link rel="stylesheet" href="components/loader.css">
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <ons-sliding-menu menu-page="view/menu.html" main-page="view/navigator.html" side="left"
            var="slidingMenu" type="reveal" max-slide-distance="200px" swipeable="false"
            behind-page="view/menu.html" above-page="view/home.html">
    </ons-sliding-menu>
    <script src="js/app.js"></script>
    <script src="js/controller/camera.js"></script>
    <script src="js/controller/menu.js"></script>
    <script src="js/controller/home.js"></script>
    <script src="js/controller/pref.js"></script>
    <script src="js/controller/rest.js"></script>
    <script src="js/controller/rest_detail.js"></script>
    <script src="js/controller/freeword.js"></script>
    <script src="js/controller/map.js"></script>
    <script src="js/service/camera.js"></script>
    <script src="js/service/pref.js"></script>
    <script src="js/service/rest.js"></script>
    <script src="js/service/rest_detail.js"></script>
    <script src="js/service/freeword.js"></script>
    <script src="js/service/map.js"></script>
</body>
</html>
```

UIを構成しているのは僅か以下の行のみとなる。

```
<ons-sliding-menu menu-page="view/menu.html" main-page="view/navigator.html" side="left"
            var="slidingMenu" type="reveal" max-slide-distance="200px" swipeable="false"
            behind-page="view/menu.html" above-page="view/home.html">
</ons-sliding-menu>
```

上記を解説すると、ons-sliding-menuコンポーネントを主軸に定義し、menuとして左サイドからスワイプで開かれるページを「view/menu.html」と定義している。

```
menu-page="view/menu.html"
side="left"
```

そして、今回はtabbarとの共存をさせたかったため、以下のように記述することで実現している。

```
main-page="view/navigator.html"
```

これにより、**tabbarページをメインページに表示し、スワイプでmenu.htmlが表示されるコンポーネント**が生成されるわけである。

また、この「ons-」で始まるUIコンポーネントがOnsenUIが提供するコンポーネントとなります。
ドキュメントをみると様々なUIとともに、サンプルやAPIの概要と、紹介も豊富に用意されているため、あまり迷うことなく使えると思う。
http://ja.onsen.io/guide/overview.html

あと、実は今回はons-sliding-menuはヘッダーの「三」をタップした時にしか表示されない挙動にしました。
それを実現しているコードが先ほどのons-sliding-menuの以下です。

```
swipeable="false"
```

これで画面をスライドしてもメニューが表示されない仕様になります。
なぜこうしたかというと、実は以下の二点で問題が発生したからです。

**・メニュー画面から「ホーム」を押すと、次回以降スライドしてメニューを出すとスライディングメニューがいくつも重なって表示される**

**・地図画面でスライディングメニューと地図の回遊が衝突してうざい**


なのでfalseにしてやりました。

## navigator.htmlについて

続いて、メニュー画面の基盤として設定されたnavigatorについて説明です。

navigator.html

```
<ons-tabbar var="tabbar">
    <ons-tabbar-item
        icon="home"
        label="ほーむ"
        page="view/home.html"
        active="true"></ons-tabbar-item>
    <ons-tabbar-item
        icon="camera"
        label="れぽーと"
        page="view/camera.html"></ons-tabbar-item>
<!--    <ons-tabbar-item
        icon="gear"
        label="設定"
        page="view/setting.html"></ons-tabbar-item>
-->
</ons-tabbar>
```

ons-tabbar-itemに指定したページが画面下部のタブバーで選択・表示できる画面となります。

```
<ons-tabbar-item
        icon="home"
        label="ほーむ"
        page="view/home.html"
        active="true"></ons-tabbar-item>
```

また、上記のように、「active=true」を指定すると、デフォルトで選択されるページを指定できます。
今回はhome.htmlを指定しています。
iconに指定している「home」はFont AwesomeもしくはIoniconsから選択できます
詳しくは[コチラ](http://ja.onsen.io/reference/ons-icon.html)を参照。

## home.htmlについて

アプリ起動時のファーストビューがこの画面になります。
この画面では以下の機能を使用してます。

1.should-spin
2.現在地緯度経度取得
3.input textのスマホライクUI構築
4.ng-show
5.class="button button--outline"
6.pushPage()とpopPage()

### should-spin

```
<ons-button should-spin={{isSpin}} ng-click="getGeoLocation();">現在地図から探す</ons-button>
```

ここで使ってます。**ons-button**はOnsenUIが提供する、スマホチックなボタンコンポーネントです。色々カスタマイズ可能で、[ココ](http://ja.onsen.io/reference/css.html#button)見ると色々とデザイン変えられるのがわかる。5番の透明ボタンもここを参照して指定しました。

[ココ](http://ja.onsen.io/reference/ons-button.html)見ると、ons-buttonのAPIが紹介されており、属性のとこに今回の「should-spin」が記載されてます。
これを指定することで、以下の画像のような読み込み中をボタンの中であらわせます。

![spin.png](https://qiita-image-store.s3.amazonaws.com/0/78857/0993447b-7f9e-962e-3213-787dcf48e136.png)

いつshould-spinを解除するかは、今回はAngularの双方向データバインディングを利用して実現しています。

```
should-spin={{isSpin}}
```

{{isSpin}}はhome.jsで以下のように定義・使用されており、

```
$scope.isSpin = false;
```

isSpin変数の値がtrueになると、spinが発動し、falseになるとSpinがとまる挙動をとる。

### 現在地緯度経度取得について

現在地緯度経度の取得には**Cordovaの「Geolocation Plugin」**を使用しています。

Monacaで開発中のアプリにCordovaのプラグインを導入するには、以下の画面キャプチャにあるように、「設定」→「Cordovaプラグインの管理」から設定できます。

![geolocation.png](https://qiita-image-store.s3.amazonaws.com/0/78857/f5e1fa22-f98a-d42c-767b-fa9f851dc11e.png)

Geolocationプラグインを有効にすることで、PJの設定ファイルにimportされ、アプリ内で現在地の取得APIが使用可能となります。
※仕組みとしては、javascriptinterface経由でネイティブのAPIを呼び出せるようにしている。

プラグインを導入するだけで、AndroidManifestに反映されるかまでは忘れたが、以下にAndroidManifestが格納されているため、権限が必要ならここのファイルを編集することで使用可能になります。

![manifest.png](https://qiita-image-store.s3.amazonaws.com/0/78857/a95acf82-3290-84fb-6bf7-2d629eb21c07.png)

最低限上記の権限(permission)が定義されていれば問題ない。

実際の使い方は以下

まず、ボタンのイベント発行から

```
<ons-button should-spin={{isSpin}} ng-click="getGeoLocation();">現在地図から探す</ons-button>
```

ng-click="getGeoLocation();"この記述で、ボタンクリック時にHomeControllerの$scopeからgetGeoLocation()関数を呼び出しています。

HomeControllerのgetGeoLocation()は以下

```
$scope.getGeoLocation = function() {
        $scope.isSpin = true;
        monaca.showSpinner();
        navigator.geolocation.getCurrentPosition(onSuccess, onError, {maximumAge: 3000, timeout: 30000, enableHighAccuracy: true});
};
```

実際にCordovaの機能を使って、現在地取得APIをコールしているのは以下

```
navigator.geolocation.getCurrentPosition(onSuccess, onError, {maximumAge: 3000, timeout: 30000, enableHighAccuracy: true});
```

navigatorはOnsenUIのnavigationと混同しがちだが、これはcordovaが提供するコンポーネントをさす。なので常に「navigator」。
詳細は[ココ](http://cordova.apache.org/docs/en/2.5.0/cordova_geolocation_geolocation.md.html)のドキュメントを参照。

第一引数がAPI成功時
第二引数が失敗時
第三引数がオプションの指定となる。

オプションには接続タイムアウトに30秒、精度の高い緯度経度取得(GPS)の設定を表している。

これだけで現在緯度経度が取得できる、あとは成功時の以下の処理から緯度経度を抽出し、サービスを介して次の画面に設定している。

```
function onSuccess(position) {
        $scope.isSpin = false;
        var latlng = {
            lat: 0,
            lng: 0
        };
        latlng.lat = position.coords.latitude;
        latlng.lng = position.coords.longitude;
        SetLatlngService.latlng = latlng;
        homeNavigator.pushPage('view/map/map.html');
};
```

コールバックで「position」が得られるので、そこから取得された緯度経度をサービスにセットしています。
なお、サービスはコントローラ宣言時に

```
app.controller('HomeController', function($scope, FreewordInfoService, SetLatlngService) {
```

と、SetLatlngServiceをDIしているため、使用可能となる。
最後にhomeNavigatorからmap.htmlにpushして、スタックに積みつつ、画面遷移を実現しています。

![alt](http://image.slidesharecdn.com/ng-japan-150518144519-lva1-app6892/95/onsen-ui-24-638.jpg?cb=1436155113)
※引用元(http://www.slideshare.net/AsialCorp/onsen-ui)

### input textのスマホライクUI構築

フリーワードのUIはAndroid4系のholoイメージにしている

これも[ココ](http://ja.onsen.io/reference/css.html#text-input)見て当て込んだ感じです。

### ng-show

これは完全にAngularの機能だが、表示・非表示を簡単に定義できる。使い方イメージは先ほどのspinと同じで、「ng-show="isPalShown"」とし、isPalShown自体はjsに記載されている。

```
$scope.isPalShown = true;
```

今回はテキストボックスにフォーカスが当たったら、パルが邪魔になるので、以下のようにどかしています。

▼html

```
<input type="text" ng-focus="onFocusFreeword();" ng-blur="onFocusOutFreeword();" id="freeword" placeholder="例) 南流山 フレンチ" class="text-input text-input--underbar"/>
```

▼js

```
// フリワードテキストからフォーカスが外れた時
$scope.onFocusOutFreeword = function() {
        $scope.isPalShown = true;
};
```

blurがfocusイベントを検知して、jsにつないでいる。

今回はここまで。続きは別途記事書きます。
navigator周りの処理をまだまとめきれていないので、整理がついたら記載します。