Background Notification (Silent Push)

# 機能概要

- 特徴
    - 画面上には表示されないサイレント通知を発行することができる
    - アプリはこれを受けたタイミングで何かしらの処理を起動することができる
    - 通知自体にPayloadとしてデータを載せることもできるし、通知受信をフックとしてのみ使い、そのタイミングで通信を開始させるといったことも可能
- 通知を受けた際のフック処理
    - 実行可能時間
        - 30秒間
    - 実行可能な状態
        - ユーザーによって明示的に終了されていない状態
            - （システムによってkillされている状態の場合はバックグラウンドで叩き起こしてくれる）
    - 実行タイミング
        - システムが判断する（保留、遅延される可能性がある）
- 注意点
    - 以下のような理由から、配信を必ず受け取ることができる保証がされていない
        - BackgroundNotificationはシステムの中で優先度が低く扱われるので、必ずしも即時で実行されるとは限らない
        - 通知の数が大量になるとシステム側で配信の制御が行われる可能性がある
            - 許可される量はシステムのその時点での状態に依存するが、1時間に2〜3回を超えて送信しないことが推奨されている（[公式ドキュメント](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/pushing_background_updates_to_your_app)に記載あり）
        - システムが通知の配信を保留・遅延させることがある
            - 追加で新しい通知を受け取ると古い通知は破棄される
            - アプリが強制終了された場合保留された通知は破棄される
            - 保留通知はユーザーの操作によりアプリが起動された際にはすぐにシステムから配信される
    - アプリ起動中は必ず通知を受信することができる
- 所感
    - 実行保証がされておらず、間引かれたり遅延したりする可能性があるため、必ず実行させたいような処理をこれで担保するのは難しい
    - アプリ起動中であれば、通知が間引かれたりすることはないので、フォアグラウンド時のリアルタイム更新といった用途で使うこともできそう

# 有効化する方法

※ 以下の設定はPush通知を送信するための基本的な設定は終わっている前提

### 通知リクエスト側

■ HTTP header
iOS13からは以下を設定することが推奨されている

```md
- apns-priority = 5
- apns-push-type = background
```

■ HTTP body
alert, sound, badgeの設定は付与せずに、content-availableを1に設定する
Payloadの指定も可能

```json
{
    “aps”: {
        “content-available”: 1
    },
    "acme1" : "bar",
    "acme2" : 42
}
```

### アプリ側

`Capability > BackgroundMode > Remote notifications` を有効化する

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41486/5c701ea8-fb00-95ba-7063-0d02e355be8a.png"/>

# サンプルコード

https://github.com/chocoyama/BackgroundSamples/blob/master/BackgroundSample/AppDelegate.swift
※ 直接関係ない実装も含まれています :bow: 

### 実装概要

BackgroundNotificationをアプリが受信した際、AppDelegateの `didReceiveRemoteNotification` が呼び出されるので、ここで30秒以内に完了できる処理を記述する。
処理の完了後はcompletionHandlerを呼び出してシステムに完了を通知。
`newData, noData, failed` の3種類の結果を返すことができ、この返却値に応じて以降のバックグラウンド処理のスケジューリングが行われたり、AppSwitcher上でのスナップショット画面の更新が行われたりする。

```swift
@UserDefault(key: .latestBackgroundPushUpdateDate, defaultValue: nil)
private var latestBackgroundPushUpdateDate: Date?

func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    self.latestBackgroundPushUpdateDate = Date()
    NotificationHelper.postLocalNotification(with: Message(body: "Received Background Push"))

    // do something

    DispatchQueue.main.async {
        completionHandler(.noData)
    }
}
```

# 検証

### 準備

検証のために以下の準備を行い、実際に通知を送信してみた。

- Apple Push Notification Authentication Keyの作成
    - https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/establishing_a_token-based_connection_to_apns
- デバイストークンの取得
    - https://developer.apple.com/documentation/usernotifications/registering_your_app_with_apns
- BackgroundNotification送信スクリプトの作成
    - 以下のライブラリを利用
        - https://github.com/node-apn/node-apn

### push通知送信コード

```js
const apn = require('apn');

const deviceToken = '<通知対象デバイスのデバイストークン>';
const options = {
    token: {
        key: '<AuthenticationKey （.p8ファイル） が置いてあるパス>',
        keyId: '<AuthenticationKeyのID （DeveloperCenter上に表示されている）>',
        teamId: '<DeveloperアカウントのチームID>',
    },
    production: false,
};
const apnProvider = new apn.Provider(options);

const notification = new apn.Notification();
notification.topic = '<アプリのBundleID>';
notification.contentAvailable = true;
notification.priority = 5;
notification.pushType = 'background';

apnProvider.send(notification, deviceToken).then((result) => {
    console.log(result);
});
```

### 結果

検証端末： `iPhone11Pro iOS13.3.1`

- アプリがフォアグラウンドの状態の時
    - 毎回必ず `didReceiveRemoteNotification` が呼び出された
- アプリがバックグラウンド状態の時
    - 即時に `didReceiveRemoteNotification` が呼び出される時もあれば、そうでない時もあった
    - 即時で呼び出されなかった時、アプリを起動してみると、そのタイミングで該当のハンドラが呼び出される挙動は確認できた
        - しかし、呼び出されないこともあったので、これは連続で送信し過ぎて間引かれた可能性がありそう
    - 複数回通知を送信した場合でも、アプリ起動時には1度しか `didReceiveRemoteNotification` が呼び出されなかった
        - これは、「追加で新しい通知を受け取ると古い通知は破棄される」という仕様によるものだと思われる
- フォアグラウンド・バックグラウンド共通
    - HTTPヘッダーに `apns-priority, apns-push-type`を指定しなかった場合ハンドラが呼び出されなくなった。

### 検証できていないこと

- システムによってkillされた状態からアプリが叩き起こされる挙動
    - [ドキュメント](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623013-application) に記載されていた仕様だが、状況の再現が難しいため確認ができなかった

# 参考
- https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/pushing_background_updates_to_your_app
- https://developer.apple.com/videos/play/wwdc2019/707/
- https://developer.apple.com/documentation/usernotifications/registering_your_app_with_apns
- https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623013-application