iOSにおけるFirebase Cloud Messagingによるプッシュ通知の実装方法

# これは？
[Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging) を使用してプッシュ通知を実装することがあったので、それについてまとめたもの。

## 前提事項
環境はiOS10以降を想定しており、つまりは [UserNotifications
 Framework](https://developer.apple.com/documentation/usernotifications) を使用します。

また、本項ではサーバーサイドの実装については言及しません。

## Apple Push Notification serviceとは？
**Apple Push Notification service (APNs)** は、Appleが提供するプッシュ通知を実現するサービスです。

プッシュ通知を担う登場人物としては

- 通知を受信する側の **アプリ**
- 通知をアプリに送信するサーバーである **プロバイダー** 
- **APNs**

の大きく3つが存在し、以下のような関係で結ばれています。

<img src=https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Art/remote_notif_simple_2x.png>

受信側と送信側の通知の設定が完了したら、プロバイダーからAPNsに送信のリクエストを投げると対象のデバイスに通知が届くというわけです。

もし通知を受け取った時にアプリが起動されていなくても、通知自体は表示されます。さらにデバイス自体が起動していない場合は、APNsがしばらく待機したのちに再度通知を送信してくれるようです。

## Firebase Cloud Messagingとは？
[公式ドキュメント](https://firebase.google.com/docs/cloud-messaging) から引用すると、

> Firebase Cloud Messaging（FCM）は、メッセージを無料で確実に配信するためのクロスプラットフォーム メッセージング ソリューションです。

です。

iOSだとAPNsがあるようにAndroid, Webとそれぞれに対応したプロバイダーをこちらが実装するのは手間でしょう。

それらのプラットフォームごとの実装差異を吸収してくれるのがFCMです。

## 実装手順
さっそく実装の方法について見ていきます。

### 0. Firebase Cloud Messaging の設定をする
[こちらの公式ドキュメント](https://firebase.google.com/docs/cloud-messaging/ios/client) 通りに実装するだけです。
よって、以下の説明はほとんどが公式ドキュメントからの引用になります。

#### 0.1. アプリに Firebase を追加する
1. **[iOS アプリに Firebase を追加]** をクリックして設定手順に沿って操作します。
1. メッセージが表示されたら、アプリのバンドル ID を入力します。必ずアプリで使用しているバンドル ID を入力してください。バンドル ID を設定できるのは、アプリを Firebase プロジェクトに追加するときだけです。
1. Firebase iOS 構成ファイルをアプリに追加します。
    1. **[GoogleService-Info.plist をダウンロード]** をクリックして、Firebase iOS 構成ファイル（GoogleService-Info.plist）を入手します。
    1. 構成ファイルを Xcode プロジェクトのルートに移動します。メッセージが表示されたら、構成ファイルをすべてのターゲットに追加するオプションを選択します。

#### 0.2. SDK の追加
Firebase SDKをプロジェクトに追加します。
CocoaPodsが推奨されているので、そちらの方法について書いていきます。

1. インストールするポッドを追加します。次のようにして Podfile にポッドを含めます。

  ```ruby
  pod 'Firebase/Core'
  pod 'Firebase/Messaging'
  ```

2\. ポッドをインストールし、.xcworkspace ファイルを開いて Xcode でプロジェクトを確認します。

#### 0.3. アプリで Firebase を初期化する
Firebase 初期化コードをアプリケーションに追加する必要があります。Firebase モジュールをインポートして、次に示すように共有インスタンスを構成します。

1. `UIApplicationDelegate` で Firebase モジュールをインポートします。
2. `FirebaseApp` 共有インスタンスを `FirebaseApp.configure()` メソッドを呼び出して構成します。通常はアプリケーションの `application:didFinishLaunchingWithOptions:` メソッドで行います。

#### 0.4. APNs 認証キーをアップロードする
APNs 認証キーを Firebase にアップロードします。
認証キーは以下の手順で取ってきます。

1. デベロッパー アカウントで **[Certificates, Identifiers & Profiles]** に移動し、**[Keys]** の下の **[All]** を選択します。

2. 右上隅にある **[Add]** ボタン（+）をクリックします。

3. APNs Auth Key の説明を入力します

4. **[Key Services]** で **[APN]** チェックボックスをオンにし、**[Continue]** をクリックします。

5. **[Confirm]** をクリックし、ダウンロードします。キーを安全な場所に保存します。このダウンロードは 1 回限りであり、後でキーを取得することはできません。

これにより取得したAPNs認証キーを以下の手順でFirebaseにアップロードしましょう。

1. Firebase コンソールのプロジェクト内で歯車アイコンを選択し、**[プロジェクトの設定]**、**[クラウド メッセージング]** タブの順に選択します。

1. **[iOS アプリの設定]** の下の **[APNs 認証キー]** で **[アップロード]** ボタンをクリックします。

1. キーが保存されている場所に移動し、キーを選択して **[開く]** をクリックします。キーのキー ID（[Apple Developer Member Center](https://developer.apple.com/membercenter/index.action) の **[Certificates, Identifiers & Profiles]** で確認できます）を追加し、**[アップロード]** をクリックします。

### 1. CapabilitiesからPush Notificationsをオンする
Xcodeで `Capabilities > Push Notifications` をオンにします。
<img width="686" alt="スクリーンショット 2019-05-03 13.57.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153095/1ade3bbc-b4cc-c9a9-70c5-6a7a6c85d23c.png">

### 2. 通知の許可をユーザーに要求する
プッシュ通知を送信する前に、まずはユーザーの許可を取る必要があります。

以下のメソッドを呼び出してあげましょう。

```swift
UNUserNotificationCenter.current()
    .requestAuthorization(options: [.alert, .sound, .badge]) {
      granted, error in
      ...
    }
```

呼び出してあげるタイミングとしては [WWDC2018の「Designing Notifications」セッション](https://developer.apple.com/videos/play/wwdc2018/806/) によると

> Do not prompt on first launch
Explain why notifications are valuable
Present at a relevant moment

らしいです。

その通知が必要だとユーザーが感じる、関連するタイミングで通知の許可を要求するのがいいみたいですね。

また、 [公式ドキュメント](https://developer.apple.com/documentation/usernotifications/unusernotificationcenter/1649527-requestauthorization) によると
> Always call this method before scheduling any local notifications and before registering with the Apple Push Notification service. Typically, you call this method at launch time when configuring your app's notification support.

とも書いているため、初回の表示タイミングについては前述の通り配慮しつつ、2回目以降は起動時に毎回呼び出してあげる、というのが良さそうですね。

### 3. APNsに登録してデバイストークンを受け取る
[公式ドキュメント](https://developer.apple.com/documentation/usernotifications/registering_your_app_with_apns) によると、

> You register your app and receive your device token each time your app launches using Apple-provided APIs.

とあるように、アプリが起動するたびにAPNsに登録する必要があります。

`2.` にてユーザーが通知の許可をしていることが確認できたら ( `granted == true` であれば) 以下のメソッドを呼び出してAPNsに登録しましょう。

```swift
UIApplication.registerForRemoteNotifications
```

登録の結果に応じて **通常は** 以下のDelegateメソッドが呼び出されます。

```swift
// 成功したら
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
  ...
}

// 失敗したら
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
  ...
}
```

今回は **Firebase Cloud Messagingでの実装** のため、以下のメソッドを代わりに実装します。

```swift
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String) {
  ...
}
```

このメソッドはFIRMessagingのデリゲートメソッドであるため、 `FirebaseApp.configure()` の後に以下のデリゲートを設定してあげましょう。

```swift
Messaging.messaging().delegate = self
```

もしアプリを起動中にデバイストークンが変更されたら、 このメソッドが再び呼び出されます。

よって、このメソッド内で受け取ったデバイストークンは都度サーバーへ送り、常に最新のデバイストークンをサーバーがAPNs対して指定できるようにします。

(この際に、サーバー側で古いデバイストークンを正しく更新するために、デバイスを識別するidをテーブルの定義に載せておくと良さそうです。)

### 4. リモート通知のハンドリング処理を書く
通知受け取りの方法は、アプリの状態や通知の種類によって大きく3つに分かれます。

#### アプリがフォアグラウンドで通知を受け取ったとき
`UNUserNotificationCenterDelegate` の以下のメソッドが呼び出されます。

```swift
UNUserNotificationCenter.current().delegate = self
```

```swift
func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        willPresent notification: UNNotification,
        withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
        ) {
  ...
}
```

フォアグラウンドでも通知を受け取るためには、引数である `completionHandler` に [適切なオプション](https://developer.apple.com/documentation/usernotifications/unnotificationpresentationoptions) を渡して通知の表示方法を指定しましょう。

#### ユーザーが通知バナーをタップしたとき
同じく `UNUserNotificationCenterDelegate` のメソッドである以下のメソッドが呼び出されます。

```swift
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
  ...
}
```

こちらのメソッドで受け取った `response` からペイロードをparseして適切なシーンの表示や処理を実行してあげます。

#### Silent Push Notification を受信したとき
サイレントプッシュは、アプリをバックグラウンドの状態で何か処理を実行させたい場合に使用します。

iOSアプリ側でサーバーに対してポーリングする必要がないという点ではかなり効果的ですね。

使用するためにはまずは `Capabilities` から `Background Modes` の `Remote notifications` をオンにしてあげる必要があります。

<img width="592" alt="スクリーンショット 2019-05-03 13.58.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153095/f88a8ff6-92b8-3f4e-4b91-632bb06bb6bc.png">

次に、以下のAppDelegateのメソッドを実装します。

```swift
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
  ...
}
```

このメソッド自体は以下のタイミングで呼び出されます。

1. アプリ未起動時の通知バナータップ
2. フォアグラウンド時に通知を受け取ったとき
3. バックグラウンド時の通知バナータップ
4. `content_available : 1` が乗ったペイロードの通知を受け取ったとき

基本的には、1, 2, 3のタイミングでの通知は `UserNotifications Framework` のメソッドでハンドリングし、 `4.` のタイミングでの通知のみをこのメソッドではハンドリングするのが良いと思います。

よって、サイレントプッシュを実装していないのであれば、こちらのメソッドも定義する必要はないでしょう。

## 最後に
細かい中身の実装は省きましたが、以上がiOSにおけるFirebase Cloud Messagingによるプッシュ通知の実装でした。

誤っている箇所などございましたら指摘していただけると幸いです。

## 参考
[APNs Overview](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/APNSOverview.html)

[Push Notifications Tutorial: Getting Started](https://www.raywenderlich.com/8164-push-notifications-tutorial-getting-started)

[プッシュ通知受信時に呼ばれるメソッドについて](http://xyk.hatenablog.com/entry/2016/11/02/180204)

[iOS に Firebase Cloud Messaging クライアント アプリを設定する](https://firebase.google.com/docs/cloud-messaging/ios/client)