Ionicで半モーダルを作成する

(この記事はIonicアドベントカレンダー2018の9日目の記事です。
この記事はKineca Developer Blogにアップしたものをアドカレ用に修正したものです。
興味ある方[KinecaDeveloperBlog](https://medium.com/kineca-developer)もぜひどうぞ！）


本記事は半モーダルをIonicで実装する方法について解説しています（最近急増中のあれです）
半モーダル知らない人は以下のリンクをクリック。
[iOSにおける半モーダルビューの解釈](https://note.mu/usagimaruma/n/n0c608c327e0c)

Ionicで半モーダルを実装する場合、次の手順を踏めば実装できます。

1. ModalControllerを改造して実装する
2. メインのコンテンツはion-footer内に記述する
3. ion-contentは透過させるようにする
4. 背景を黒くさせるbackdropを用意する

これらを実装すると、次のようになります。
<img src="https://qiita-image-store.s3.amazonaws.com/0/165335/a9b6440a-386d-3ac4-788a-c45102e14fdb.gif" width=300>
※上の画像は弊社のエンタメアプリ「pato」に実装された様子になります。興味ある方は以下のリンクをクリック。 
https://pato.today/

本記事では、上のような半モーダルを実装する方法を紹介しています。興味のある方は是非とも続きを読んでみてください。
**「ソースコードだけみたいんだ！」**って人は一番下にあるのでスクロールしてください。


## 新しくIonicアプリケーションを作る

新しくIonicアプリケーションを生成します。次のコマンドを実行してください。

```bash
ionic start ionic-semi-modal blank
```

生成が完了したら `ionic-semi-modal` ディレクトリに移動します。

```bash
cd ionic-semi-modal
```

次は、半モーダル用のページを生成しましょう

```shell
ionic g page semi-modal
```

また、その半モーダルをhome画面から呼び出せるようにする必要があります。

`src/pages/home/home.ts`を次のように変更します。

```typescript
import { Component } from "@angular/core";
import { NavController, ModalController } from "ionic-angular";

@Component({
  selector: "page-home",
  templateUrl: "home.html"
})
export class HomePage {
  counter: number = 0;
  constructor(
    public navCtrl: NavController,
    public modalCtrl: ModalController
  ) {}

  showSemiModal() {
    const modal = this.modalCtrl.create("SemiModalPage", {
      counter: this.counter
    });
    modal.onDidDismiss(res => {
      if (res !== null) {
        this.counter = res;
      }
    });
    modal.present();
  }
}
```

合わせて`src/pages/home/home.html`と`src/pages/home/home.scss`を次のように変更しましょう。

```html
<ion-header>
  <ion-navbar>
    <ion-title>
      Ionic SemiModal
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <div class="counter-wrapper">{{counter}}</div>
  <button ion-button (click)="showSemiModal()">Show SemiModal</button>
</ion-content>
```

```scss
page-home {
  ion-content {
    text-align: center;
  }
  .counter-wrapper {
    font-size: 60px;
  }
}
```

## 半モーダルを作成する
半モーダル自体の作成は非常に簡単です。Ionicにはion-footerというコンポーネントがありこれを利用すれば簡単に半モーダルのUIを作成することができます。
src/pages/semi-modal/semi-modal.html、semi-modal.scssを以下のように書き換えましょう

```html
<ion-content (click)="close()">
</ion-content>

<ion-footer padding>
  <div class="semi-modal-content">
    <button ion-button icon-only (click)="down()">
      <ion-icon name="remove"></ion-icon>
    </button>
    <div class="counter">
      {{counter}}
    </div>
    <button ion-button icon-only (click)="up()">
      <ion-icon name="add"></ion-icon>
    </button>
  </div>
  <button ion-button full color="secondary" (click)="save()">Save</button>
</ion-footer>
```

```scss
page-semi-modal {
  ion-content {
    opacity: 0;
  }
  ion-footer {
    background-color: #fff;
    .semi-modal-content {
      display: flex;
      justify-content: center;
      align-items: center;
      padding-bottom: 20px;
      .counter {
        font-size: 60px;
      }
      button {
        margin: 0px 12px;
      }
    }
  }
}
```

各ボタンに対するアクションも記述します。

```typescript
import { Component } from "@angular/core";
import { IonicPage, ViewController, NavParams } from "ionic-angular";

@IonicPage()
@Component({
  selector: "page-semi-modal",
  templateUrl: "semi-modal.html"
})
export class SemiModalPage {
  counter: number = 0;

  constructor(public viewCtrl: ViewController, public navPrams: NavParams) {
    this.counter = this.navPrams.data.counter;
  }

  ionViewDidLoad() {
    console.log("ionViewDidLoad SemiModalPage");
  }

  up() {
    this.counter++;
  }

  down() {
    this.counter--;
  }

  close() {
    this.viewCtrl.dismiss(null);
  }
  save() {
    this.viewCtrl.dismiss(this.counter);
  }
}
```

以上、半モーダルのページが完成します。

<img src="https://qiita-image-store.s3.amazonaws.com/0/165335/dfbc047d-1ff9-e425-fa41-9fc4f2cc3395.gif" width=300>

## バックドロップを自作する
半モーダルのページが完成しましたが、今のままだと背景が薄暗くなりません。単純に考えるとion-contentのbackdround-colorを変更すれば良さそうですが、その方法だとモーダルの遷移アニメーションと一緒に背景が移動してしまうという問題があります。
<img src="https://qiita-image-store.s3.amazonaws.com/0/165335/3928ee85-8531-700f-3e55-1d7ca3cca499.gif" width=300/>

この問題に対応するためにbacdropを自作します。新しくBackdropProviderを作成しましょう。

```shell
ionic g provider backdrop
```

src/providers/backdrop/backdrop.tsを以下のように変更しましょう。

```typescript
import { Injectable } from "@angular/core";

@Injectable()
export class BackdropProvider {
  constructor() {}

  show() {
    let backdrop = document.createElement("div");
    backdrop.id = "custom-backdrop";
    backdrop.className = "backdrop-fade-in";
    backdrop.style.position = "absolute";
    backdrop.style.top = "0";
    backdrop.style.right = "0";
    backdrop.style.left = "0";
    backdrop.style.bottom = "0";
    backdrop.style.width = "100%";
    backdrop.style.height = "100%";
    backdrop.style.background = "#000000";
    backdrop.style.opacity = "0.4";
    backdrop.style.display = "none";
    backdrop.style.pointerEvents = "none";

    let ionApp: any = document.getElementsByTagName("ion-app")[0];
    if (ionApp) {
      ionApp.appendChild(backdrop);
      backdrop.style.display = "block";
    }
  }

  hide() {
    let backdrop = document.getElementById("custom-backdrop");
    if (backdrop) {
      backdrop.classList.add("backdrop-fade-out");
      setTimeout(() => {
        backdrop.remove();
      }, 300);
    }
  }
}
```

backdropのfade-in、fade-outのアニメーションを`src/app/app.scss`に追加します。

```scss
.backdrop-fade-in {
  animation-name: fadeInOpacity;
  animation-timing-function: ease-in;
  animation-duration: 250ms;
  animation-fill-mode: both;
}

@keyframes fadeInOpacity {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 0.4;
  }
}

.backdrop-fade-out {
  animation-name: fadeOut;
  animation-duration: 200ms;
  animation-timing-function: ease-out;
  animation-fill-mode: both;
}
@keyframes fadeOut {
  0% {
    opacity: 0.4;
  }
  100% {
    opacity: 0;
  }
}
```

最後に、semi-modal表示時にbackdropを呼び出すようにします。src/pages/semi-modal/semi-modal.tsを以下のように変更しましょう。

```diff
import { Component } from "@angular/core";
import { IonicPage, ViewController, NavParams } from "ionic-angular";
+import { BackdropProvider } from "../../providers/backdrop/backdrop";

@IonicPage()
@Component({
  selector: "page-semi-modal",
  templateUrl: "semi-modal.html"
})
export class SemiModalPage {
  counter: number = 0;

+  constructor(public viewCtrl: ViewController, public navPrams: NavParams, public backdrop: BackdropProvider) {
    this.counter = this.navPrams.data.counter;
+    this.backdrop.show();
  }

  ionViewDidLoad() {
    console.log("ionViewDidLoad SemiModalPage");
  }

  up() {
    this.counter++;
  }

  down() {
    this.counter--;
  }

  close() {
+    this.backdrop.hide();
    this.viewCtrl.dismiss(null);
  }
  save() {
+    this.backdrop.hide();
    this.viewCtrl.dismiss(this.counter);
  }
}
``` 




## 完成
以上、半モーダルが完成しました。実際に完成したアプリケーションは下のように動作します。非常に簡単に実装できますね。

<img src="https://qiita-image-store.s3.amazonaws.com/0/165335/d7280c89-3f3e-2319-fbe3-c01883760c56.gif" width=300>

## ソースコード
https://github.com/scrpgil/ionic-semi-modal
※気に入ったらスターをください

## さいごに
この記事で紹介している実装は本番運用のアプリでも動いてます！
改めてアプリのリンクを載せますので興味のある方は触ってみてください！
https://pato.today/

また、patoを運営する株式会社キネカでは他にもIonicアプリを運用しております。
Ionic開発者の採用も積極的に行なっていますので興味のある方是非とも御声がけください。
それでは！