【ハンズオン】NCMBのFramework7用UIコンポーネントを使ってスマホアプリを作る

NCMBはmBaaS（mobile Backend as a Serivce）であり、UIは各フレームワークやプログラミング言語で開発する必要があります。しかし、イチからUIを作るのは大変なことです。

そこで最近、色々なUIコンポーネントを作っています。これらは個々に独立しているので、簡単にアプリ内に導入が可能です。今はFramework7向けに作っていますが、以下のようなUIコンポーネントがあります。

- [地図コンポーネント](https://qiita.com/goofmint/items/20d73a3aad5313f2ecd8)
- [お知らせコンポーネント](https://qiita.com/goofmint/items/ede45debc91df4439da5)
- [ギャラリーコンポーネント](https://qiita.com/goofmint/items/07a5bf7d747b99c16589)
- [認証コンポーネント](https://qiita.com/goofmint/items/6dc28141da611426abc4)
- [リスト・詳細コンポーネント](https://qiita.com/goofmint/items/9df40e8d4117716fe073)
- [フォームコンポーネント](https://qiita.com/goofmint/items/df11f566c35f55fce826)
- [スワイパーコンポーネント](https://qiita.com/goofmint/items/d5bfc38a30c4112dc14a)
- [設定画面コンポーネント](https://qiita.com/goofmint/items/053707151e8a3ada5da4)
- [プロフィール画面コンポーネント](https://qiita.com/goofmint/items/f2a01782bc9149f82ffa)
- [情報リスト画面コンポーネント](https://qiita.com/goofmint/items/286c934cd8445915ae31)

今回は実際にこれらのコンポーネントを使ってアプリを開発する方法を紹介します。まずアプリの仕様とSDKの初期化について解説します。

![demo-app (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/f94b192e-e53d-f9d0-9964-d3dcafff3bf4.gif)


## 完成版のコード

作成したデモアプリのコードは[NCMBMania/UIComponentApp: Framework7向けUIコンポーネントを使ったデモアプリです。](https://github.com/NCMBMania/UIComponentApp)にアップロードしてあります。

## ベースについて

ベースは[最新版のFramework7を使ったテンプレート](https://github.com/NCMBMania/MonacaFramework7Base)です。これはMonacaのFramework7テンプレートで、Framework7の最新版へアップデート + NCMBを追加したものです。

[zipファイル](https://github.com/NCMBMania/UIComponentApp_Handson/archive/refs/heads/main.zip) をダウンロードして、Monacaでインポートします。

## 画面について

今回利用している画面は次の通りです。

### [地図コンポーネント](https://qiita.com/goofmint/items/20d73a3aad5313f2ecd8)

![screenshot-4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/e0fa4b47-3529-4d3b-2882-05ab9d5fc472.jpeg)

### [認証コンポーネント](https://qiita.com/goofmint/items/6dc28141da611426abc4)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/851bc583-2f13-ecc6-fc6b-eec2858d4693.png)

### [リスト・詳細コンポーネント](https://qiita.com/goofmint/items/9df40e8d4117716fe073)

![FireShot Capture 078 - 20220218165333 List - localhost.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/77901c4e-6f71-c5fb-e9e9-162a3371f46d.jpeg)

詳細画面です。

![FireShot Capture 078 - 20220218165334 List - localhost.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/a4051d07-4194-ae66-eb19-a8b4dc58e8a1.jpeg)


### [情報リスト画面コンポーネント](https://qiita.com/goofmint/items/286c934cd8445915ae31)

![FireShot Capture 153 - 20220307133148 Information - localhost.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/de7b8163-17ad-5a30-38c8-f6e6da4d6c00.jpeg)

### 画面の配置

それぞれのHTMLファイルを `www/pages/` 以下に配置します。

- [map.html](https://raw.githubusercontent.com/NCMBMania/framework7_map_component/main/www/pages/map.html)
- [auth.html](https://raw.githubusercontent.com/NCMBMania/framework7_auth_component/main/www/pages/auth.html)
- [list.html](https://raw.githubusercontent.com/NCMBMania/framework7_list_component/main/www/pages/list.html)
- [detail.html](https://raw.githubusercontent.com/NCMBMania/framework7_list_component/main/www/pages/detail.html)
- [information.html](https://raw.githubusercontent.com/NCMBMania/framework7_information_component/main/www/pages/information.html)

## 利用するライブラリについて

NCMB JavaScript SDK以外に利用しているライブラリは次の通りです。

- moment.js
- Mapbox JS SDK
- Turf.js

これらは `www/index.html` で読み込みます。

```html
<!-- 記述済み -->
<link href='https://api.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.css' rel='stylesheet' />

<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment-with-locales.min.js" integrity="sha512-LGXaggshOkD/at6PFNcp2V2unf9LzFq6LE+sChH7ceMTDP0g2kn6Vxwgg7wkPP7AAtX+lmPqPdxB47A0Nz0cMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src='https://api.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.js'></script>
<script src='https://unpkg.com/@turf/turf@6/turf.min.js'></script>
```

## CSPの編集

さらにMapboxのWorker用にCSPを編集します。これも `www/index.html` です。

```html
<!-- 記述済み -->
<meta http-equiv="Content-Security-Policy" content="default-src * 'self' 'unsafe-inline' 'unsafe-eval' data: gap: content: blob:">
```

## 情報画面用にアイコンファイルを読み込み

今回は情報リスト画面があるので、[NCMBとFramework7を使った情報リスト画面コンポーネントの紹介と使い方 - Qiita](https://qiita.com/goofmint/items/286c934cd8445915ae31)で紹介したCSSファイルと、フォントを読み込みます。

- [style.css](https://github.com/NCMBMania/framework7_information_component/blob/main/www/css/style.css)
- [fonts](https://github.com/NCMBMania/framework7_information_component/tree/main/www/css/fonts)

```html
<!-- 記述済み -->
<link rel="stylesheet" href="css/style.css">
```

## SDKのキーを設定

今回利用するキーは次の2つです。

- [Mapbox](https://www.mapbox.com/)
- [ニフクラ mobile backend](https://mbaas.nifcloud.com/)

それぞれキーを取得したら `www/js/app.js` にてNCMBとMapboxを初期化します。 `www/js/config.json` を開いて、各キーを編集してください。

```json
// mapboxTokenを追加
{
	"applicationKey": "APPLICATION_KEY",
	"clientKey": "CLIENT_KEY",
	"mapboxToken": "MAPBOX_TOKEN"
}
```

そして `www/js/config.json` を以下のように読み込んで初期化処理に使っています。

```js
// 記述済み
// Monacaアプリの場合はdeviceready、プレビューの場合はDOMContentLoaded
const event = window.cordova ? 'deviceready' : 'DOMContentLoaded';
document.addEventListener(event, async (e) => {
  // 設定ファイルの読み込み
  const config = await (await fetch('./js/config.json')).json();
  // NCMBの初期化
  window.ncmb = new NCMB(config.applicationKey, config.clientKey);
  // Mapboxの初期化
  mapboxgl.accessToken = config.mapboxToken;
  // Framework7の初期化
  window.app = new Framework7({
    el: '#app', // App root element
    name: 'framework7-core-tab-view', // App name
    theme: 'auto', // Automatic theme detection
    routes: routes,
  });
});
```

## index.htmlの編集

まず `www/index.html` では `/tabs/` にアクセスするだけです。

```html
<!-- 記述済み -->
<div id="app">
	<div class="view view-init safe-areas" data-url="/tabs/">
	</div>
</div>
```

## tabs.htmlの作成

次に `www/tabs.html` です。このファイルは各UIコンポーネントを読み込むタブバーを実装します。テンプレート部分は次のようになります。

```html
<!-- 記述済み -->
<template>
	<div class="page">
		<div class="navbar">
			<div class="navbar-bg"></div>
			<div class="navbar-inner">
				<div class="left"></div>
				<div class="title">メイン画面</div>
				<div class="right">
					${login ?
						$h`<a class="link" href="#" @click=${logout}>
							<i class="f7-icons">person_badge_plus_fill</i>
						</a>`	 
					:
					$h`<a class="link" href="/login/">
								<i class="f7-icons">person</i>
							</a>`
					}
				</div>
			</div>
		</div>
		<div class="toolbar tabbar-labels toolbar-bottom">
			<div class="toolbar-inner">
				<a href="./" class="tab-link" data-route-tab-id="tab1">
					<i class="icon f7-icons">book</i>
					<span class="tabbar-label">ニュース</span>
				</a>
				<a href="./map/" class="tab-link" data-route-tab-id="tab2">
					<i class="icon f7-icons">map</i>
					<span class="tabbar-label">地図</span>
				</a>
				<a href="./information/" class="tab-link" data-route-tab-id="tab3">
					<i class="icon f7-icons">exclamationmark_circle</i>
					<span class="tabbar-label">情報</span>
				</a>
			</div>
		</div>
		<div class="tabs tabs-routable">
			<div class="page-content tab" id="tab1"></div>
			<div class="page-content tab" id="tab2"></div>
			<div class="page-content tab" id="tab3"></div>
		</div>
	</div>
</template>
```

そしてJavaScript部分では認証状態に応じて `login` 変数を変えて、UI側でもログイン状態によって表示を変えています。

```html
<!-- 記述済み -->
<script>
  export default function (props, {$f7, $on, $update}) {
		let login;
		$on('page:beforein', () => {
			// ログインチェック
			login = !(ncmb.User.getCurrentUser() === null);
			$update();
		});
		// ログアウト処理
		const logout = async () => {
			await ncmb.User.logout();
			login = false;
			$update();
		};
		return $render;
	}
</script>
```

最後に `js/routes.js` を開いてルーティングを追加します。タブバーでは初期表示が `/` のコンポーネント呼び出しになります。リスト・情報コンポーネントでは表示するFeedクラスのobjectIdが必要なので、propsで与えています。このobjectIdはアプリによって異なるので注意してください（後述）。

```js
// 記述済み
const routes = [
  {
    path: '/',
    url: './index.html',
  },
  {
    path: '/tabs/',
    componentUrl: './pages/tabs.html',
    options: {
      props: {
        objectId: '2YAtnb0nxjjfbeZ3', // 後ほど書き換えます
      },
    },
    tabs: [
			// リスト・詳細コンポーネント用
      {
        id: 'tab1',
        path: '/',
        componentUrl: './pages/list.html',
      },
			// 地図コンポーネント用
      {
        id: 'tab2',
        path: '/map/',
        componentUrl: './pages/map.html',
      },
			// 情報リスト用
      {
        id: 'tab3',
        path: '/information/',
        componentUrl: './pages/information.html',
      },
    ]
  },
  // Default route (404 page). MUST BE THE LAST
  {
    path: '(.*)',
    url: './pages/404.html',
  },
];
```

## Feed/Entryクラスの作成

詳細は[NCMBとFramework7を使ったリスト・詳細コンポーネントの紹介と使い方 - Qiita](https://qiita.com/goofmint/items/9df40e8d4117716fe073)に譲りますが、[スクリプト](https://raw.githubusercontent.com/NCMBMania/UIComponentApp_Handson/main/feed.js)を使ってRSSフィードをデータストアに登録します（スクリプトのアプリケーションキー、クライアントキーは書き換えてください）。

スクリプトはGETにて登録し、 パラメータとして `url=フィードのURL` として実行してください。当ハンズオンで利用するフィードのURLです。

```
https://blog.mbaas.nifcloud.com/feed
```

問題なく完了すれば FeedクラスとEntryクラスができあがりますので、そのFeedクラスのobjectIdを上記 `routes.js` のobjectIdに適用してください。

## 一覧からの画面遷移

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/c1cf1551-ce75-a7c4-8b52-8c915f770c65.png)

記事一覧から詳細画面へ遷移するルーティングを `routes.js` に追加します。 `404.html` のルーティング設定の上に追加してください。

```js
// 記述済み
{
	path: '/list/entries/:objectId',
	componentUrl: './pages/detail.html',
},
// ↑追加
// Default route (404 page). MUST BE THE LAST
{
	path: '(.*)',
	url: './pages/404.html',
},
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/374a92c8-3ad6-41de-1314-72bd6f74e47f.png)


これでリスト・詳細コンポーネントの実装が完了です。

## 認証コンポーネントの追加

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/dd2d223e-756b-7b59-168c-06cb284dce77.png)


認証コンポーネント用のルーティングを `routes.js` に追加します。このコンポーネントでは、画面遷移する前に認証状態を確認しています。もしすでに認証している場合にはトップ画面に戻ります。

```js
// 記述済み
{
	path: '/',
	url: './index.html',
},
// ↓追加
{
	path: '/login/',
	componentUrl: './pages/auth.html',
	// 画面遷移前に認証状態を確認
	beforeEnter: function ({ resolve, reject, router }) {
		const user = ncmb.User.getCurrentUser();
		// 認証状態チェック
		if (user) {
			// 認証済み
			reject();
			router.navigate('/');
		} else {
			// 認証前
			resolve();
		}
	}
},
```

認証コンポーネントの呼び出しは、すでに `tabs.html` に記載済みです。 `/login/` で認証コンポーネントが呼び出されます。

```html
<!-- 記載済み -->
<div class="right">
	${login ?
		$h`<a class="link" href="#" @click=${logout}>
			<i class="f7-icons">person_badge_plus_fill</i>
		</a>`	 
	:
	$h`<a class="link" href="/login/">
				<i class="f7-icons">person</i>
			</a>`
	}
</div>
```

### 認証コンポーネントの修正

認証コンポーネントでは認証後にどこに遷移するかは設定していないので、その部分を修正します。 `pages/auth.html` を開いて修正します。 `$f7router.back()` を有効化して、認証後に前の画面に戻るようにします。

```js
try {
	// ログイン処理
	await ncmb.User.loginWithMailAddress(email, password);
	app.dialog.alert('ログインしました', 'ログイン成功');
	// ↓ コメントアウトされているのを外す
	$f7router.back(); // 戻る場合
} catch (e) {
	// エラーの場合はログインエラー
	app.dialog.alert('メールアドレスとパスワードを確認してください', 'ログインエラー');
}
```

### 管理画面の設定変更

NCMBの管理画面にて、メールアドレス認証を有効にしてください。

## 地図画面

![screenshot-4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/e0fa4b47-3529-4d3b-2882-05ab9d5fc472.jpeg)


### データのインポート

今回は[store.json](https://raw.githubusercontent.com/NCMBMania/UIComponentApp/main/stores.json)を読み込んで利用します。NCMBの管理画面にて `Store` クラスとしてインポートしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/f408964f-331a-7317-66ea-1e021e9bba6f.png)

このデータフォーマットは次のようになっています。

| フィールド名 | 型 |
|------------|------------|
| name | 文字列 |
| geo | 位置情報
| address | 文字列 |
| tel | 文字列 |

後は地図タブを表示するだけで、このデータを表示できます。

## 情報画面

![FireShot Capture 153 - 20220307133148 Information - localhost.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/de7b8163-17ad-5a30-38c8-f6e6da4d6c00.jpeg)

### データの作成

NCMBの管理画面にて `Information` クラスを作成します。そしてフィールドを作成します。

| フィールド名 | 意味 |
|------------|------------|
| label | 一覧に出す名前 |
| color | アイコンの色 |
| icon | Framework7のアイコン名 |
| path | URLまたはFramework7のルーティングパス |

アイコン名は[Framework7 アイコン](https://framework7.jp/icons/)にて調べられます。iconはその他、line/hatebu/feedlyが利用できます。

これもデータを作成するだけで表示できます。