Expoでのプッシュ通知の仕方についてまとめ（ローカル通知編）

[Stack Overflow Developer Survey 2020](https://insights.stackoverflow.com/survey/2020#technology-programming-scripting-and-markup-languages)によれば、世界で最も使われているプログラミング言語は8年連続でJavaScriptでした。
名実ともに、エンジニアにとって最も馴染み深い言語だと思います。
そして、このJavaScriptを使ってモバイルアプリケーション開発をできるようにしたフレームワークが**React Native**です。
React Nativeを使えば、JavaScript(React)でiOS/Android双方のアプリ開発ができます。

React Nativeの開発支援ツールは色々とあると思うのですが、本稿では**[Expo](https://expo.io/)**を使います。
**Expo**はオープンソースのツールチェインで、クラウドビルド、ローカルでの実機テスト、カメラや位置情報の利用、OTAアップデート等の様々な機能を簡単に実装することができます。
以降の項では、**Expoを使ってローカル通知をする方法**を解説します。


## ローカル通知とは？

<img width="377" alt="プッシュ通知イメージ図" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282311/721013e9-0506-11af-04d2-b2b3dfb773bc.png">

プッシュ通知を使えば、アプリが起動中かに関わらず、そのアプリに関する情報をユーザーに通知することができます。
プッシュ通知には**ローカル通知**と**リモート通知**の２種類があり、それぞれ実装方法等が異なります。

**ローカル通知**は、アプリから直接システムに対してプッシュ通知の配信を要求します。
システムは指定されたコンテンツや配信条件（日時や位置情報等）に基づいて、ユーザーへ通知を表示します。
オフラインでも利用することができ、配信に遅延が生じないため、リマインダーやカレンダーアプリ等で便利だと思います。

一方、**リモート通知**の場合はオンラインで利用します。
まず、ユーザーのデバイストークン[^1]を取得し、自社で用意したアプリケーション・サーバーへ保存しておきます。
アプリケーション・サーバーはプッシュ通知を配信するタイミングで、デバイストークンと配信するコンテンツを併せて、AppleやGoogleが提供するプッシュ通知配信サーバーへ配信依頼を出します。
複数のユーザーに対して一括で通知することができるため、マーケティング関係のメッセージを配信したり、サービス提供元から何らかのアナウンスをする際に便利ですが、配信において通信が必要なため配信遅延等が生じる可能性があります。

ローカル通知もリモート通知も、ユーザーからすればどちらも同じように見えます。
Expoを使えばどちらも容易に実装することができますが、前述の通り、本稿では**ローカル通知**の実装方法について記載します。


## セットアップ

**Expo**を使えば、まるでRuby on Railsのようにコマンド一つでアプリケーションの雛形を作ることができます。
そのためにいくつか必要なツールがありますので、それらも踏まえたセットアップ手順を記載します。
※ なお本稿では開発環境として macOS Catalina 10.15.6 を使用しています。また本稿執筆時点（2020/08/10）での手順になりますので、最新の情報は[こちらから](https://expo.io/learn)ご確認ください。

###### 1. `Expo Client` をスマートフォンにインストールする。

`Expo Client` は開発中のモバイルアプリケーションの実機テストを行うためのアプリです。
[iOS版](https://itunes.apple.com/app/apple-store/id982107779)と[Android版](https://play.google.com/store/apps/details?id=host.exp.exponent&referrer=www)の両方がありますので、お手持ちの端末に合わせてインストールしてください。

###### 2. `Node.js` をダウンロードする。

`Node.js` はJSエコシステムを使うために必要になります。[最新のLTS版](https://nodejs.org/en/)の使用を推奨します。
※ 本稿では v12.18.3 を使用しています。

###### 3. `expo-cli` をインストールする。

`expo-cli` はプロジェクトの新規作成やサーバーの起動、ビルドなどを**Expo**で行うためのコマンドラインツールです。
以下のコマンドでインストールできます。

```bash
$ npm install expo-cli --global
```
※ なお、本稿でのバージョンは 3.23.3 です。

###### 4. プロジェクトを立ち上げる

```bash
$ expo init localNotificationTest --npm -t blank
$ cd localNotificationTest 
```

###### 5. `expo-notifications` をインストールする

`expo-notifications` はExpoでプッシュ通知を行うためのモジュールです。
以下のコマンドでインストールできます。

```bash
$ expo install expo-notifications
```

###### 6. サーバーを立ち上げる。

```bash
$ npm start
```

###### 7. QRコードをスキャンする。

iOSの場合はカメラで、Androidの場合はExpo Clinentでスキャンすると、コードが読み込まれます。
`Open up App.js to start working on your app!` というテキストが表示されたら成功です。

## ユーザーに通知権限を要求する

さて、プッシュ通知をデバイスに表示するためには、事前にユーザーから許可（通知権限）を得る必要があります。
`expo-notifications` はこうした機能もカバーしています。
まずファイルを以下のように書き換えます。

```javascript:localNotificationTest/App.js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import * as Notifications from 'expo-notifications';

export default function App() {
  React.useEffect(() => {
    requestPermissionsAsync();
  })

  return (
    <View style={styles.container}>
      <Text>Open up App.js to start working on your app!</Text>
    </View>
  );
}

const requestPermissionsAsync = async () => {
  const { granted } = await Notifications.getPermissionsAsync();
  if (granted) { return }

  await Notifications.requestPermissionsAsync();
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
    flexDirection: 'column',
  },
});
```

React Nativeは**Fast Refresh機能**がありますので、ファイルを保存すると自動的に変更がExpo Clinentにも反映されると思います。
もしもうまく変更が反映されなかった場合は、Expo Clinentの画面を三本指でタップし続けると表示されるメニュー画面から「Reload」を選択してください。
以下のようなポップアップが表示されれば成功です。
<img width="377" alt="通知権限リクエストイメージ図" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282311/372e8ffa-9dfb-35ee-3cb0-f2d26af96ca3.png">

通知権限に関するAPIについて少し解説します。
`expo-notifications` は、現時点の通知権限の情報を取得する `getPermissionsAsync` と、ユーザーに通知権限を要求するポップアップを出す `requestPermissionsAsync`という2つのメソッドを用意しています。
どちらも非同期関数で実装されており、戻り値としてPromiseを返します。
これ以外にも 〜Async という命名の関数はすべて非同期関数になります。
呼び出す際は、async関数 / await演算子 と組み合わせて使うと便利だと思います。

また、`requestPermissionsAsync`メソッドについて、一点だけ注意点があります。
iOS でも Androidでも、あるアプリから**繰り返し同じ権限リクエストをすることは禁止**されています。
したがって、アプリで過去に一度でも `requestPermissionsAsync` を使った場合は、それ以降は通知権限リクエストのポップアップを表示することはできません。

しつこくポップアップを出すとユーザーに不快感を与えてしまうため、この制約はプロダクション環境では問題ないのですが、開発環境でExpo Clinentを使って通知権限周りのテスト（例: ポップアップでDon't Allowを選択した場合にあるべき振る舞いになるか？）をする場合は問題になることがあります。
Expo Clinentをインストールし直さなければ、権限リクエストの再テストができないためです。

iOSシミュレーター(Android Studioエミュレーター)を使えばこの問題はある程度緩和することができます。
一旦シミュレーター内でExpo Clinentを削除し、再び`$ npm start`コマンドでサーバーを立ち上げ直せば、自動的にExpo Clinentが再インストールされるためです。
iOS ミュレーターをセットアップする場合は[こちらを](https://docs.expo.io/workflow/ios-simulator/)、Android Studio エミュレーターをセットアップする場合は[こちら](https://docs.expo.io/workflow/android-studio-emulator/)の記事をご参考ください。

## アプリがバックグラウンドで起動/クローズしているときにプッシュ通知する

プッシュ通知を実際に送信する場合は、`scheduleNotificationAsync` というメソッドを使います。
例えば60秒後にプッシュ通知したい場合は、以下のように書きます。

```javascript
import * as Notifications from 'expo-notifications';

Notifications.scheduleNotificationAsync({
  content: {
    body: 'テスト',
  },
  trigger: {
    seconds: 60,
  },
});
```

引数に渡したオブジェクトの `content` プロパティが配信内容を表し、  `trigger` プロパティが配信時刻などの配信条件を表します。
いずれも必須のプロパティです。

タップすると3秒後にプッシュ通知を配信するボタンを追加するために、サンプルコードを以下のように書き換えてみます。

```javascript:localNotificationTest/App.js
import React from 'react';
import { StyleSheet, View, Button } from 'react-native';
import * as Notifications from 'expo-notifications';

export default function App() {
  React.useEffect(() => {
    requestPermissionsAsync();
  })

  return (
    <View style={styles.container}>
      <Button
        title='3秒後にプッシュ通知する'
        onPress={scheduleNotificationAsync}
      />
    </View>
  );
}

const scheduleNotificationAsync = async () => {
  await Notifications.scheduleNotificationAsync({
    content: {
      body: 'test'
    },
    trigger: {
      seconds: 3,
    }
  })
}

const requestPermissionsAsync = async () => {
  const { granted } = await Notifications.getPermissionsAsync();
  if (granted) { return }

  await Notifications.requestPermissionsAsync();
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
    flexDirection: 'column',
  },
});
```

以下のような挙動になったらうまく行っていると思います。
![push_notification_after_3s.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282311/930b0df7-916b-1f31-193b-b068968f5e62.gif)

#### プッシュ通知をカスタマイズする

プッシュ通知の内容をカスタマイズしたい場合は、 `content` プロパティにわたすオブジェクトを変更します。
例えば上述のプッシュ通知にタイトルをつける場合は以下のように書きます。

```javascript
content: {
  body: 'test',
  title: 'title'
}
```

以下のようにタイトルが表示されます。
<img width="373" alt="プッシュ通知のタイトルイメージ図" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282311/e62dee6c-f314-eded-303d-d72ae60365f4.png">

`subtitle` プロパティをつけると、タイトルと本文の間に副題をつけることができます。

```javascript
content: {
  body: 'test',
  title: 'title',
  subtitle: 'subtitle'
}
```
<img width="383" alt="プッシュ通知のサブタイトルイメージ図" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282311/ba67f156-9256-1a2a-f7e2-93d81c0be10f.png">


`content` プロパティにわたすオブジェクトで使える代表的なプロパティは以下になります。

|プロパティ名|値|
| ------- | ------- |
| body | プッシュ通知の本文 |
| title | 太字で表示されるタイトル |
|subtitle| iOSの場合、タイトルと本文の間に挿入される太字のサブタイトル<br>Androidの場合は端末ごとの仕様に依存する|
|sound|プッシュ通知を受信したときの着信音の設定<br>値がnull, またはプロパティ自体が省略されている場合は着信音なしに<br>値が`'default'` または `'defaultCritical'` または `'custom'` の場合は着信音を鳴らす|
|badge|通知バッジ数を指定した数字にする（後述）|
|data|ユーザーには表示されないが、プッシュ通知を介してアプリから取得できるデータ（オブジェクト）をセットする|

その他の`content` プロパティで使える設定は[こちらから](https://docs.expo.io/versions/latest/sdk/notifications/#notificationcontentinput)確認できます。
<br>
配信時刻を変更する場合は `trigger` プロパティで指定します。
`trigger` プロパティは、渡す値のデータのタイプによって振る舞いが変わります。

例えば、現在時刻から3秒後にプッシュ通知する場合は以下のように書きます。

```javascript
trigger: {
  seconds: 3,
}
```

60秒ごとに繰り返し通知する場合は以下になります。
※ 繰り返し通知する場合は、少なくとも1分以上の間隔をあける必要があります。

```javascript
trigger: {
  repeats: true,
  seconds: 60,
}
```

配信時刻をUnix時刻[^5]で指定する場合は以下になります。

```javascript
trigger: Date.now() + (1000 * 3)
```

毎日15時00分に通知する場合は以下になります。

```javascript
trigger: {
  hour: 15,
  minute: 00,
  repeats: true,
}
```

<br>
以上が基本的なスケジュール設定のサンプルになります。
もっと複雑なスケジューリングをしたい場合はどうでしょうか？
例えば毎月第一土曜日の15時00分に通知する場合は以下になります。

```javascript
trigger: {
  weekday: 7,
  weekdayOrdinal: 1,
  hour: 15,
  minute: 00,
  repeats: true
}
```

ただし、この記法は**iOSの場合にしか使えません**。
`expo-notifications` が内部的に`UNCalendarNotificationTrigger` というiOSのネイティブコードをラップしているためです。
利用できる条件は[こちら](https://docs.expo.io/versions/latest/sdk/notifications/#calendartriggerinput)をご参照ください。

## アプリがフォアグラウンドのときに通知する

アプリが起動中のタイミングでプッシュ通知を受信した場合、そのプッシュ通知をどう扱うかはアプリ側に委ねられます。
**Expoのデフォルトではプッシュ通知は破棄されます。**

アプリが起動中であろうとユーザーに表示したい場合は `setNotificationHandler` というメソッドで設定します。

サンプルコードを次のように書き換えてください。

```javascript
import React from 'react';
import { StyleSheet, View, Button } from 'react-native';
import * as Notifications from 'expo-notifications';

Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: false,
    shouldSetBadge: false,
  }),
});

// 以下同じです
```

以下のように、アプリが起動中でもプッシュ通知を表示できるようになります。
![フォアグラウンドでの表示イメージ動画](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282311/bcf77966-d35b-1fac-af37-022c89ac3a85.gif)

## プッシュ通知をキャンセルする

さて、プッシュ通知をキャンセルする方法について見ていきます。
特にリピート設定をした場合は手動でキャンセルしない限り永遠に通知されてしまいます。
`expo-notifications` では、プッシュ通知をキャンセルする方法を2つ用意しています。

#### すべてのプッシュ通知をキャンセルする

現在、スケジュールされているすべてのプッシュ通知をキャンセルする場合は以下のAPIを使います。

```javascript
Notifications.cancelAllScheduledNotificationsAsync();
```

#### 特定のプッシュ通知をキャンセルする

この場合、キャンセルしたい**プッシュ通知のID**を取得する必要があります。
`getAllScheduledNotificationsAsync` メソッドを使うと、スケジュールされているすべてのプッシュ通知情報を配列で取得できます。

```javascript
const notifications = await Notifications.getAllScheduledNotificationsAsync();
const identifier = notifications[0].identifier; 
console.log(identifier) // 例:  "a2772de7-e1ce-4a18-ab1d-8f4f3102dea7"
```

※ 取得できるデータについては[こちら](https://docs.expo.io/versions/latest/sdk/notifications/#notificationrequest)からご確認ください。

IDを取得できたら、今度はそれを `cancelScheduledNotificationAsync` メソッドに渡します。

```javascript
Notifications.cancelScheduledNotificationAsync(identifier);
```

以上の方法で、指定したプッシュ通知をキャンセルできます。

## イベントリスナーを登録する

Expoではプッシュ通知に関するイベントリスナーを登録できます。
これにより、**プッシュ通知関係のイベントが発生したときに自動的にコードを発火させることができます。**

#### アプリがフォアグラウンドのとき

プッシュ通知を受信したタイミングでコードを発火させる場合は、 `addNotificationReceivedListener` メソッドを使います。

```javascript
const subscription = Notifications.addNotificationReceivedListener(e => {
  console.log(e.request.content.body);
})
subscription.remove();
```

イベントリスナーには引数として `Notification`[^10] イベントが渡されます。
`Notification` イベントからはプッシュ通知のコンテンツやID等の情報を取得できます。

また、`addNotificationReceivedListener` メソッドは戻り値として Subscription[^11] オブジェクトを返します。
このオブジェクトの `remove`メソッドを使えば、今回登録されたイベントリスナーを解除できます。

#### アプリがバックグラウンド / 停止中のとき

アプリがバックグラウンドの時に、プッシュ通知の受信をトリガーにコードを自動実行する方法はあるでしょうか？
残念ながらExpoではそうした機能はサポートしていません。

代わりに、 `addNotificationResponseReceivedListener` メソッドを使って、**プッシュ通知をユーザーがタップしたときに発火させるイベントリスナー**を登録できます。

```javascript
const subscription = Notifications.addNotificationResponseReceivedListener(e => {
  console.log(e.notification.request.content.body);
})
subscription.remove();
```

イベントリスナーには引数として `NotificationResponse`[^12] イベントが渡されます。
`addNotificationReceivedListener` メソッドと同様に、 `remove` メソッドで購読を解除できます。

※ `addNotificationReceivedListener` メソッドと `addNotificationResponseReceivedListener` メソッドでは、それぞれイベントのデータのタイプが異なる点にご注意ください。

## 通知バッジを操作する

アプリに**通知バッジ**をつけたり、通知バッジ数を変更したい場合があります。
通知バッジとは、ホームスクリーンのアプリアイコンのそばに表示される数字つきのアイコンのことです。
<img width="91" alt="通知バッジイメージ図" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282311/45c5742f-379c-cf2b-3543-1bc9c12cc7a3.png">

Expoではこの通知バッジを操作する方法を二通り用意しています。

#### 手動で設定する

`setBadgeCountAsync` メソッドを使えば、アプリが起動中のときに通知バッジを操作できます。

```javascript
Notifications.setBadgeCountAsync(5);
```

引数で指定した数字の通知バッジをアプリにセットします。
※ なお、引数を 0 にすると、通知バッジを取り消すことができます。

#### プッシュ通知のペイロードで指定する

プッシュ通知のコンテンツに `badge` プロパティを渡すと、指定した通知バッジ数に変更できます。

```javascript
await Notifications.scheduleNotificationAsync({
  content: {
    body: 'test',
    badge: 1,
  },
  trigger: {
    seconds: 3
  }
})
```

プッシュ通知を受信したタイミングで自動的に通知バッジ数が変更されます。
![プッシュ通知のペイロードで通知バッジ数指定する](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282311/b73a45fd-56c8-0f05-a377-b19bd75eeb35.gif)

※ なお、この方法でアプリが起動中のときに通知バッジを変更する場合は、 `setNotificationHandler` メソッドで設定が必要です。

```javascript
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: false,
    shouldSetBadge: true,
  }),
});
```



`shouldSetBadge: true` にすれば、プッシュ通知の受信をきっかけに通知バッジを操作できます。

[アプリがバックグラウンドまたは停止中のときに、自動的に通知バッジ数をインクリメント（デクリメント）する方法はありません。](https://forums.expo.io/t/how-to-increment-push-notification-badge-counter-when-app-is-in-the-background/10312/4)
そのため、基本的にはユーザーの通知バッジ数はサーバーで管理しておく必要があると思います。

## まとめ

`expo-notifications` を使えばプッシュ通知を簡単に実装できます。
またExpoには他にも様々なモジュールがあります。
例えば [expo-permissions](https://docs.expo.io/versions/latest/sdk/permissions/) というモジュールを使えば、通知権限以外の権限リクエスト（例: 位置情報）を行うことができます。
[expo-constants](https://docs.expo.io/versions/latest/sdk/constants/) というモジュールを使えば、アプリケーションのライフサイクル中で変らないシステム情報（例: シミュレーターか実機かどうか）を取得できます。

本番用のアプリケーションを開発する上でこうしたモジュールは便利だと思いますので、本稿の結びとして紹介します。

## 参考リンク

[expo-notifications公式ドキュメント](https://docs.expo.io/versions/latest/sdk/notifications/)


[^1]: デバイスごとの一意の識別キー。プッシュ通知における宛先のこと。
[^5]: 協定世界時 の1970年1月1日深夜0時からの経過ミリ秒数のこと。
[^10]: [Notificationオブジェクト APIリファレンス](https://docs.expo.io/versions/latest/sdk/notifications/#notification)
[^11]: [Subscriptionオブジェクト APIリファレンス](https://docs.expo.io/versions/latest/sdk/notifications/#subscription)
[^12]: [NotificationResponseオブジェクト APIリファレンス](https://docs.expo.io/versions/latest/sdk/notifications/#notificationresponse)