IonicとFirebaseを使って3日で作る写真共有アプリ - Viewの作成 -

本記事は「IonicとFirebaseを使って3日で作る写真共有アプリ」シリーズの記事です。
まとめ記事↓
https://qiita.com/kazuki502/items/585aef0d79ed1235bec0

アプリの画面を作るところを説明していきます。

これは私の感覚なんですが、アプリを作るときはまず画面と画面遷移から作ったほうがスムーズに進むと思っています。見た目を作ってから内部処理を作っていく感じですね。今回もそんな風に作っていきます。

## Ionicのフォルダ構成について説明（少し理論的なので後から読んでもOKです）
画面を作っていく前に、Ionicのフォルダ構成について簡単に説明しておきます。Ionic（ベースとなっているAngularも同様）の基本単位はModuleと言われる塊です。まず一つの大きなModuleがあり、その中に複数のModuleが含まれる構成になっています。Moduleは何個でもModuleを含むことが出来て、ページ単位や機能単位でまとめるのが一般的な構成です。

```ionic start```を実行した直後のフォルダ構成はこのようになっているはずです。

```
src─┬─app─┬─home─┬─home.module.ts
    │     │      ├─home.page.ts 
          │      ├─home.page.html
          │      ├─home.page.scss
          │
          ├─app.module.ts
          ├─app.component.ts
          ├─app.component.html
          ├─app.component.scss
          ├─...
```
この中には、2つのmoduleがあります。"app.module.ts"というmoduleの中に"home.module.ts"というmoduleが含まれているという関係になっています。
"app.module.ts"は、アプリ全体を包含しているroot moduleであり、"home.module.ts"はアプリの関連する機能を集約したfeature moduleとなっています。"app.module.ts"の直下にすべての機能を書くこともできるんですが、メンテナンス性が悪くなったり、パフォーマンスを改善しにくくなったりするので良くない構成です。"home.module.ts"のように適切に分割されていると、分かり易くなるし、"lazy load"という初期起動時間を短縮できるテクニックも使えるので、適切に分割するようにしましょう（初めは難しいですが…）。

## 画面の構成
今回作る画面は画面です。
1. ホーム画面
2. 写真アップロード画面（モーダル画面）
3. 写真一覧画面
4. 共有された写真再生画面
※2.は操作性を踏まえてモーダル画面にしています。

### ホーム画面の作成
まずは各画面のハブとなるホーム画面から作っていきます。画面の構成から、3つの画面に遷移するためのボタンが必要だと分かります。IonicではデザインされたUI要素がいくつも用意されているので、自分で実装する前にそこに使えるパーツが無いか見てみましょう。

Ionic UI components
https://ionicframework.com/docs/components

``` ion-button ```というのがあるので、これを"home.page.html"に追加してみましょう。ページ下部にある"Usage"に使い方の例があるので、"Angular"タブのコードを全部コピーしちゃいましょう。

```html:home.component.html
<ion-header>
  <ion-toolbar>
    <ion-title>
      Ionic Blank
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <div class="ion-padding">
    The world is your oyster.
    <p>If you get lost, the <a target="_blank" rel="noopener" href="https://ionicframework.com/docs/">docs</a> will be your guide.</p>
  </div>
  <!-- ここから追加した部分 -->
  <!-- Default -->
  <ion-button>Default</ion-button>

  <!-- Anchor -->
  <ion-button href="#">Anchor</ion-button>

  <!-- Colors -->
  <ion-button color="primary">Primary</ion-button>
  <ion-button color="secondary">Secondary</ion-button>
  <ion-button color="tertiary">Tertiary</ion-button>
  <ion-button color="success">Success</ion-button>
  <ion-button color="warning">Warning</ion-button>
  <ion-button color="danger">Danger</ion-button>
  <ion-button color="light">Light</ion-button>
  <ion-button color="medium">Medium</ion-button>
  <ion-button color="dark">Dark</ion-button>

  <!-- Expand -->
  <ion-button expand="full">Full Button</ion-button>
  <ion-button expand="block">Block Button</ion-button>

  <!-- Round -->
  <ion-button shape="round">Round Button</ion-button>

  <!-- Fill -->
  <ion-button expand="full" fill="outline">Outline + Full</ion-button>
  <ion-button expand="block" fill="outline">Outline + Block</ion-button>
  <ion-button shape="round" fill="outline">Outline + Round</ion-button>

  <!-- Icons -->
  <ion-button>
    <ion-icon slot="start" name="star"></ion-icon>
    Left Icon
  </ion-button>

  <ion-button>
    Right Icon
    <ion-icon slot="end" name="star"></ion-icon>
  </ion-button>

  <ion-button>
    <ion-icon slot="icon-only" name="star"></ion-icon>
  </ion-button>

  <!-- Sizes -->
  <ion-button size="large">Large</ion-button>
  <ion-button>Default</ion-button>
  <ion-button size="small">Small</ion-button>
</ion-content>
```
保存して```ionic serve```コマンドを実行すると下のような画面が出てきます。

<img height="400px" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69277/3c87d21f-ae2b-c1dc-62e3-822dcb375973.png">

コードと画面を比べながら、好みのボタンを選びましょう。私は"LARGE"と書かれている大きなボタンが気に入ったので、これでボタンを作ることにしました。先ほどのソースから余計なものを消して、3つのボタンを作ります。

```html:home.page.html
<ion-header>
  <ion-toolbar>
    <ion-title>
      Ionic Blank
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <div class="ion-padding">
    The world is your oyster.
    <p>If you get lost, the <a target="_blank" rel="noopener" href="https://ionicframework.com/docs/">docs</a> will be your guide.</p>
  </div>
  <ion-button size="large">ボタン１</ion-button>
  <ion-button size="large">ボタン２</ion-button>
  <ion-button size="large">ボタン３</ion-button>
</ion-content>
```
ボタンの名前は適当に付けています（後から変えていきます）。するとこのような画面になります。
<img height="400px" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69277/dcb5fdfd-dab2-b46c-b108-e8ace3eeadd2.png">

いい感じに並んでくれていますね。このようにIonicを使うと、htmlだけでそれなりのデザインの画面が作れるのがメリットの一つです。（素のAngularを使うと、ボタンのスタイルや位置を整えるのにcssも触らないといけないので結構大変です。）

このままでもいいのですが、少し寂しいので背景画像を適当に差し替えてしまいましょう。

ここで少scssファイルを修正します。

```scss:home.page.scss
ion-content {
    --background: #fff url("画像のURL") no-repeat center center / cover;
}
```
そうするとこのように背景画像が設定されます。

<img height="400px" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69277/18f78374-cf53-d9ba-ef62-4cb872ac8b9f.png">

これでホーム画面の実装は一旦ここまでにします。次は、他の画面も作っていきましょう。

### 写真アップロードモーダル画面の作成
次は、写真アップロードモーダル画面を作っていきます。モーダル画面ていうのは、もともと見ていた画面の上にかぶさるように表示される画面で、ユーザーが操作するまでは親画面を触らせないというものです。

このモーダル画面を一から実装しようとすると結構大変なんですが、Ionicにはモーダル画面を簡単に作れるコンポーネントが用意されています。

Ionic ion-Modal
https://ionicframework.com/docs/api/modal

このページにモーダル画面の設定方法が書いてあるのでそれに従って作っていきます。ざっとこんな手順です。
1. モーダル画面コンポーネントを作成する（```$ Ionic g component home/modal-upload```）
2. モーダル画面コントローラーを設定する
3. モーダル画面のスタイルを変更する

順番に進めていきましょう。

#### モーダル画面コンポーネントを作成する
まずコンソール上で次のコマンドをたたいて下さい。

```console
$ ionic g component home/modal-upload
```
homeフォルダの下に、modal-uploadフォルダが下記のような構成で作られます。

```
src─┬─app─┬─home─┬─home.module.ts
                 ├─home.page.ts 
                 ├─home.page.html
                 ├─home.page.scss
                 └─modal-upload─┬─modal-upload.component.html
                                ├─modal-upload.component.scss
                                ├─modal-upload.component.ts
```

<detail>
<summary>【コラム】 "page"と"component"の違い</summary>
Angularを勉強している方は気になったと思うので、ここで私なりの解釈をまとめておきたいと思います。Angularでは"page"というものは無くて、"component"が画面のコンポーネントとして使われているので混乱しやすいです（紛らわしいよね）。

Ionicでは、ユーザーが見える画面を"page"という塊で作成し、その画面で使う要素を部品として外に出したものを"component"として作成しているという理解をしています。つまり、"component"単体で画面表示に使われることは無くて、"page"単体、もしくは、"page"と"component"のセットとして使われるということです。

ここら辺、もっと詳しい方がいたら教えてください！
</detail>

ここまでで、モーダル画面自体は作成したのですが、まだ画面に表示できません。次は今作った画面をモーダル画面として表示する処理を設定していきましょう。

#### モーダル画面コントローラーを設定する

Ionicに標準装備されている、"modal-controller"を使うと面倒な実装をほとんどすることなくモーダル画面を設定することが出来ます。

ion-modalの[説明ページ](https://ionicframework.com/docs/api/modal)のusageを参考に、"home.page.ts"ファイルを整えていきましょう。

```typescript:home.page.ts
import { Component } from '@angular/core';
import { ModalController } from '@ionic/angular'; // 追加
import { ModalUploadComponent } from './modal-upload/modal-upload.component'; // 追加

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {

  constructor(
    public modalController: ModalController // 追加
  ) {}

  // 追加メソッド↓
  async presentModal() {
    const modal = await this.modalController.create({
      component: ModalUploadComponent, // さっき作ったComponentを指定
      componentProps: { value: 123 }
    });
    return await modal.present();
  }
}
```

```html:home.page.html
<ion-header>
  <ion-toolbar>
    <ion-title>
      Ionic Blank
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <div class="ion-padding">
    The world is your oyster.
    <p>If you get lost, the <a target="_blank" rel="noopener" href="https://ionicframework.com/docs/">docs</a> will be your guide.</p>
  </div>
  <!-- ボタンの名前の変更とクリック時の処理を追加 -->
  <ion-button size="large" (click)="presentModal()">写真アップロード</ion-button>
  <ion-button size="large">ボタン２</ion-button>
  <ion-button size="large">ボタン３</ion-button>
</ion-content>
```
モーダル画面の方も設定していきましょう。

```typescript:modal-upload.component.ts
import { Component, OnInit, Input } from '@angular/core'; // Inputを追加
import { NavParams } from '@ionic/angular'; // 追加

@Component({
  selector: 'app-modal-upload',
  templateUrl: './modal-upload.component.html',
  styleUrls: ['./modal-upload.component.scss'],
})
export class ModalUploadComponent implements OnInit {

  // "value" passed in componentProps
  @Input() value: number;

  constructor(
    navParams: NavParams // 追加
  ) { }

  ngOnInit() {}

}
```
コピペだけですが、これでモーダル画面が立ち上がるようになりました。

ブラウザーで動作を確かめてみましょう。"写真アップロード"ボタンをクリックしてみてください。

<img height="400px" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69277/5f1489bc-e35b-c6f3-223d-e28ae73891ee.png">

はい、エラーになりました笑。何がいけなかったのか、エラーログを見てみましょう（エラーログをちゃんと読むと上達が早くなりますよ）。

```
core.js:15724 ERROR Error: Uncaught (in promise): Error: No component factory found for ModalUploadComponent. Did you add it to @NgModule.entryComponents?
Error: No component factory found for ModalUploadComponent. Did you add it to @NgModule.entryComponents?
```
「componentが無いですよー！@NgModule.entryComponentsにちゃんと入れましたか？」って言ってくれているみたいです。Ionic（Angularも）は、moduleが基本単位になっていて、アプリで使うソースはそこでimportして取り込んでおく必要があります。今回のエラーはその取り込み忘れが原因のようです。

"modal-upload.component"はホーム画面で使うための部品なので、"home.module.ts"に登録したいと思います。

```typescript:home.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';

import { HomePage } from './home.page';
// 新しいimportを追加
import { ModalUploadComponent } from './modal-upload/modal-upload.component'


@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    RouterModule.forChild([
      {
        path: '',
        component: HomePage
      }
    ]),
  ],
  declarations: [
    HomePage,
    ModalUploadComponent // 追加
  ],
  // entryComponentsを追加
  entryComponents: [
    ModalUploadComponent // 追加
  ]
})
export class HomePageModule {}
```
保存して画面を立ち上げて、"写真アップロード"ボタンを押してみましょう。うまくいっていれば下のような画面ににゅっと出てくるはずです。

<img height="400px" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69277/7dc3e646-d8f4-282e-94c3-779b97270bbd.png">

一般的なイメージモーダル画面に近づけるために、もう少しいじりましょう。
themeフォルダの"variables.scss"を編集します。

```scss:variables.scss
// Ionic Variables and Theming. For more info, please see:
// http://ionicframework.com/docs/theming/

/** Ionic CSS Variables **/
:root {
  /** primary **/
  --ion-color-primary: #3880ff;
  --ion-color-primary-rgb: 56, 128, 255;
  --ion-color-primary-contrast: #ffffff;
  --ion-color-primary-contrast-rgb: 255, 255, 255;
  --ion-color-primary-shade: #3171e0;
  --ion-color-primary-tint: #4c8dff;

  /** secondary **/
  --ion-color-secondary: #0cd1e8;
  --ion-color-secondary-rgb: 12, 209, 232;
  --ion-color-secondary-contrast: #ffffff;
  --ion-color-secondary-contrast-rgb: 255, 255, 255;
  --ion-color-secondary-shade: #0bb8cc;
  --ion-color-secondary-tint: #24d6ea;

  /** tertiary **/
  --ion-color-tertiary: #7044ff;
  --ion-color-tertiary-rgb: 112, 68, 255;
  --ion-color-tertiary-contrast: #ffffff;
  --ion-color-tertiary-contrast-rgb: 255, 255, 255;
  --ion-color-tertiary-shade: #633ce0;
  --ion-color-tertiary-tint: #7e57ff;

  /** success **/
  --ion-color-success: #10dc60;
  --ion-color-success-rgb: 16, 220, 96;
  --ion-color-success-contrast: #ffffff;
  --ion-color-success-contrast-rgb: 255, 255, 255;
  --ion-color-success-shade: #0ec254;
  --ion-color-success-tint: #28e070;

  /** warning **/
  --ion-color-warning: #ffce00;
  --ion-color-warning-rgb: 255, 206, 0;
  --ion-color-warning-contrast: #ffffff;
  --ion-color-warning-contrast-rgb: 255, 255, 255;
  --ion-color-warning-shade: #e0b500;
  --ion-color-warning-tint: #ffd31a;

  /** danger **/
  --ion-color-danger: #f04141;
  --ion-color-danger-rgb: 245, 61, 61;
  --ion-color-danger-contrast: #ffffff;
  --ion-color-danger-contrast-rgb: 255, 255, 255;
  --ion-color-danger-shade: #d33939;
  --ion-color-danger-tint: #f25454;

  /** dark **/
  --ion-color-dark: #222428;
  --ion-color-dark-rgb: 34, 34, 34;
  --ion-color-dark-contrast: #ffffff;
  --ion-color-dark-contrast-rgb: 255, 255, 255;
  --ion-color-dark-shade: #1e2023;
  --ion-color-dark-tint: #383a3e;

  /** medium **/
  --ion-color-medium: #989aa2;
  --ion-color-medium-rgb: 152, 154, 162;
  --ion-color-medium-contrast: #ffffff;
  --ion-color-medium-contrast-rgb: 255, 255, 255;
  --ion-color-medium-shade: #86888f;
  --ion-color-medium-tint: #a2a4ab;

  /** light **/
  --ion-color-light: #f4f5f8;
  --ion-color-light-rgb: 244, 244, 244;
  --ion-color-light-contrast: #000000;
  --ion-color-light-contrast-rgb: 0, 0, 0;
  --ion-color-light-shade: #d7d8da;
  --ion-color-light-tint: #f5f6f9;
}

/** 追加 **/
.modal-wrapper {
  --width: 80vw;
  --height: 60vh;
  --border-radius: 20px;
}
```
すると、さっきのモーダル画面がこんな風に変わります。
<img height="400px" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69277/b8d5e4af-441f-a71c-5c82-b95012ba5e32.png">
よく見る形になりました。

後は、モーダル画面に共有する画像を選択するinputと送信ボタンを追加しておきましょう。デザインは後で整えるのでテキトーで大丈夫です。

```html:modal-upload.component.html
<p>
  modal-upload works!
</p>

<label for="photo">画像を選択</label>
<input type="file" name="photo" id="photo-upload">

<ion-button>送信</ion-button>
```
こんな感じになります（かなりがたがたですが、今はそれで大丈夫です）
<img height="400px" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69277/f3eccbff-5f7b-a64e-9503-5129bbddab89.png">

### 写真一覧ページを作成
共有された写真を一覧で見るためのページを作っていきます。これは、ホーム画面とは別の新しいページなので次のコマンドで新しい"page"を作りましょう。

```console
$ ionic g page photo-list
```
このコマンドで"page"を作ると、自動的にmoduleに追加してくれるのでかなり便利です。こんな感じで追加されているのが確認できます。

```typescript:app-routing.module.ts
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: 'home', loadChildren: './home/home.module#HomePageModule' },
  // 'photo-list'が追加されている
  { path: 'photo-list', loadChildren: './photo-list/photo-list.module#PhotoListPageModule' },
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule { }
```
Ionicはページ遷移もhtmlだけで簡単に設定できます。

```html:home.page.html
<ion-header>
  <ion-toolbar>
    <ion-title>
      Ionic Blank
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <div class="ion-padding">
    The world is your oyster.
    <p>If you get lost, the <a target="_blank" rel="noopener" href="https://ionicframework.com/docs/">docs</a> will be your guide.</p>
  </div>
  <ion-button size="large" (click)="presentModal()">写真アップロード</ion-button>
  <!-- ボタン名の変更とrouterLinkを追加 -->
  <ion-button size="large" routerLink="/photo-list">写真一覧</ion-button>
  <ion-button size="large">ボタン３</ion-button>
</ion-content>
```
これで、写真一覧ボタンを押すと画面が切り替わるようになりました。
<img height="400px" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69277/4a0848e2-8ab5-9eff-dbfa-c7aba736decc.png">

ただ、このままだと写真一覧画面に映ったままホーム画面に戻れないので、"戻るボタン"もつけましょう。実はこれもすでに用意されています。

ionoic ion-back-button
https://ionicframework.com/docs/api/back-button

usageを見ると、```<ion-header>```の中にいれればいいみたいなので素直にそうしてみます。

```html:photo-list.page.html
<ion-header>
  <ion-toolbar>
    <!-- この部分を追加 -->
    <ion-buttons slot="start">
      <ion-back-button></ion-back-button>
    </ion-buttons>
    <ion-title>photo-list</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>

</ion-content>
```
これでもう一回見てみると、画面の左上に"Back"というボタンが追加されていると思います。
※ホーム画面から遷移しないと出現しないので、ブラウザーのurlの"/photo-list"を消してから操作してください。

<img height="400px" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69277/e6c8c9a1-9793-1291-2be1-8573bf2a9af0.png">

これで、ホーム画面に戻ることが出来るようになりました。
これに写真を追加してこんな風にしていきましょう（下図）。
<img height="400px" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69277/c6ad315b-abc2-a768-e507-6f43b09c1b37.png">

こんな画面にするにはどのようにすればいいでしょうか？

（考える時間…）
（考える時間…）
（考える時間…）
（考える時間…）
はい、その通りIonicのコンポーネントを使っていけばいいですね。

このコンポーネントを使っていきましょう。
Ionic ion-img
https://ionicframework.com/docs/api/img

Ionic ion-grid
https://ionicframework.com/docs/api/grid

こんな風にhtmlをいじることでこの画面にすることが出来ます。

```html:photo-list.page.html
<ion-header>
  <ion-toolbar>
    <ion-buttons slot="start">
      <ion-back-button></ion-back-button>
    </ion-buttons>
    <ion-title>photo-list</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-grid>
    <ion-row>
      <ion-col>
        <ion-img src="https://img.co-trip.jp/content/14renewal_images_l/290282/main_image.jpg"></ion-img>
      </ion-col>
      <ion-col>
        <ion-img src="https://img.co-trip.jp/content/14renewal_images_l/290282/main_image.jpg"></ion-img>
      </ion-col>
    </ion-row>
    <ion-row>
      <ion-col>
        <ion-img src="https://img.co-trip.jp/content/14renewal_images_l/290282/main_image.jpg"></ion-img>
      </ion-col>
      <ion-col>
        <ion-img src="https://img.co-trip.jp/content/14renewal_images_l/290282/main_image.jpg"></ion-img>
      </ion-col>
    </ion-row>  
  </ion-grid>
</ion-content>
```