VoIPプッシュ通知(PushKit)と標準プッシュ通知の違いについて

# はじめに

iOS 8 から導入されたVoIP用のプッシュ通知(以後、"VoIPプッシュ")。
これについて、従来からあるプッシュ通知(以後、"標準プッシュ")との違いという観点で調査した結果をまとめた。

公式に明確な記載がなく不明な点が多々あるため、実験の結果や推測も含まれるので要注意。指摘歓迎。

## 前提知識

* 標準プッシュの知識
    * 全体的なシステム構成(サーバ → Apple(APNs) → iOSアプリ)等はVoIPプッシュも標準プッシュと同様なので、プッシュ通知についての基本情報は省略。
* Swift言語の知識
    * コード例をすべてSwiftで記載しているため。おおよそ読めれば問題ない。

# VoIPプッシュとは

Apple公式ドキュメント「[Voice Over IP (VoIP) Best Practices](https://developer.apple.com/library/ios/documentation/Performance/Conceptual/EnergyGuide-iOS/OptimizeVoIP.html)」いわく

- **デバイスはVoIPプッシュが発生したときのみ省エネルギーで起動される。**<br><font color="gray">(The device is woken only when VoIP pushes occur, saving energy.)</font>
    - 備考: 標準プッシュとの(何らかの)差を示しているのか、非推奨となった[VoIPコネクションを使った方式](https://developer.apple.com/reference/cfnetwork/kcfstreamnetworkservicetypevoip)との違いを指しているは不明。
- **アクションを実行する前にユーザの応答の必要な標準プッシュとは違って、VoIPプッシュではアプリ処理に直行できる。**<br><font color="gray">(Unlike standard push notifications, which the user must respond to before your app can perform an action, VoIP pushes go straight to your app for processing.)</font>
    - 備考: 標準プッシュでも通知ペイロードに`"content-available": 1`を含めることで、"バックグランドの"アプリでは処理が可能だったが、それ以外のアプリ非アクティブ時はユーザーの通知センター操作が必要。
- **VoIPプッシュは高優先度かつ遅延なく配送されるとみなされる。**<br><font color="gray">(VoIP pushes are considered high-priority notifications and are delivered without delay.)</font>
    - 備考: 具体的な差異は不明。標準プッシュよりは高優先度かつ低遅延と思われる。
- **VoIPプッシュは標準プッシュより多くのデータを含めることが可能。**<br><font color="gray">(VoIP pushes can include more data than what is provided with standard push notifications.)</font>
    - 備考: 2016/06/15検証時点は5KBまで可能であることを確認。<br>標準プッシュでは iOS  8以降は2KB(HTTP/2プロバイダーAPIを使った場合であれば4KB）。
- **受信時にアプリが動作していない場合にアプリを再開させることが可能。**<br><font color="gray">(Your app is automatically relaunched if it’s not running when a VoIP push is received.)</font>
    - 備考: 標準プッシュではアプリが完全に終了している状態(タスクを殺した場合や端末再起動後）にプッシュ通知によるバックグラウンド処理をできない場合があるが、VoIPプッシュでは必ず処理を行える。
- **アプリがバックグラウンドで動作している場合でも、プッシュを処理するための実行時間が与えられる。**<br><font color="gray">(Your app is given runtime to process a push, even if your app is operating in the background.)</font>
    - 備考: 具体的な時間は不明。標準プッシュ（のバックグランドでの通知受信）では30秒間。

とのこと。

# 概要比較

|  | 標準プッシュ | VoIPプッシュ |
|:--|:--|:--|
| フレームワーク  | UIKit  | PushKit  |
| 対応OS  | iOS 6以降  | iOS 8以降  |
| デバイストークン長  | 32バイト(今後100バイトとなる見込み)  | 32バイト※2016/6/10検証時点(こちらも今後100バイト？)<br>※同端末同アプリでも標準プッシュのものとは異なる値で互換性はない |
| ペイロード形式(送信時) | JSON<br>※通知の設定に関する項目が"aps"プロパティのみ規定されている  | JSON<br>※すべて自由  |
| ペイロード形式(受信時) | Dictionary<br>([NSObject : AnyObject])  | Dictionary<br>([NSObject : AnyObject])  |
| ペイロード長 | iOS 7まで: 256 byteまで<br>iOS 8以降: 2 KBまで<br>※HTTP/2 では4 KBまで | 5 KBまで<br> ※2016/6/15検証時  |
| プロバイダーAPI | Binary Provider API (独自バイナリ) [非推奨]<br>APNs Provider API (HTTP/2) [推奨]  | APNs Provider API (HTTP/2)<br>※Binary Provider API (独自バイナリ)も使える模様（2016/6/15検証時点） |
| 証明書  | Apple Push Notification service SSL<BR>※本番兼開発用(production&sandbox)と開発用(sandbox)の区分  | VoIP Services Certificate<br>※本番兼開発用(production&sandbox)のみ |
| プッシュ通知受信時の通知先  | アクティブ時：アプリ<br>その他：通知として受信 (※)<br>※content-available=1の場合は"バックグランドの"アプリに通知可 | 常にアプリ<br>※完全停止状態(バックグランドにもいない)のアプリに通知にも通知可(2016/6/15検証時点) |
| 信頼性 | 公式表記「配信はベストエフォート、到達は保証しない(Delivery of notifications is a “best effort”, not guaranteed)」| 公式表記「高優先度かつ遅延なし(VoIP pushes are considered high-priority notifications and are delivered without delay.)」

# iOS API

## 標準プッシュ

### 必要条件

プッシュ通知受信でのバックグランド処理を行う場合には
プロジェクトのCapabilitiesのBackground ModesをONにして"Remote notifications"にチェックが必要だが、しない場合は不要。

### デバイス登録要求

プッシュ通知を受け付けるようにしたいタイミング(一般的は起動直後` application:didFinishLaunchingWithOptions:`)で呼び出す。

```swift
let application = UIApplication.sharedApplication()
application.registerForRemoteNotifications()

// アプリがアクティブでない場合の通知受信時に、通知表示するためにはこれも必要
let notificationTypes: UIUserNotificationType = [.Badge, .Sound, .Alert]
let notificationSettings = UIUserNotificationSettings(forTypes: notificationTypes, categories: nil)
UIApplication.sharedApplication().registerUserNotificationSettings(notificationSettings)
```

## デバイス登録成功(=デバイストークン取得)ハンドリング

デバイストークンをプッシュ通知サーバに通知する処理を書く場所。

```swift
func application(_ application: UIApplication,
    didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData)
```

## デバイス登録失敗ハンドリング

何らかの理由でデバイストークンが得られていないの対応する処理を書く場所。

```swift
func application(_ application: UIApplication,
    didFailToRegisterForRemoteNotificationsWithError error: NSError)
```

## 標準プッシュ通知受信ハンドリング

プッシュ通知を受け取った際の処理を書く場所。

実装メソッドは２種類、引数fetchCompletionHandler が無い方と有る方。
有る方を実装した場合は無い方は呼ばれない。
バックグランド通知(content-available=1を含む場合)を処理するためには有る方が必要。
有る方では処理の終わりに引数として受け取ったfetchCompletionHandler を呼んで処理が終わったことをOS側に通知する必要がある(呼ばないと実行時にその旨のログが出る）

fetchCompletionHandler 無い方

```swift
func application(_ application: UIApplication,
    didReceiveRemoteNotification userInfo: [NSObject : AnyObject])
```

fetchCompletionHandler 有る方

```swift
func application(_ application: UIApplication,
    didReceiveRemoteNotification userInfo: [NSObject : AnyObject],
        fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void)
```

### (おまけ) デバイス登録解除要求

一般的に使うシーンは限定されるかもしれないが？が、デバッグ時など意図的に登録解除状態にしたい便利。

```swift
let application = UIApplication.sharedApplication()
application.unregisterForRemoteNotifications()
```

## VoIPプッシュ

### 必要条件

プロジェクトのCapabilitiesのBackground ModesをONにしてVoice over IPにチェック。

### VoIPプッシュの有効化

標準プッシュはUIKitの一部だったが、VoIPプッシュはPushKitを使うことになるので大幅に異なる。
プッシュ通知を受け付けるようにしたいタイミング(一般的は起動直後` application:didFinishLaunchingWithOptions:`)で呼び出す。

```swift
// import PushKit // ファイル冒頭でPushKitをインポート

// var voipRegistry:PKPushRegistry! // クラスにプロパティを定義

voipRegistry = PKPushRegistry(queue: nil)
voipRegistry.delegate = instance // PKPushRegistryDelegateを採用していす
voipRegistry.desiredPushTypes = Set([PKPushTypeVoIP])

// ローカル通知で通知センター表示を行う場合は通知を得ておくことが必要
// let notificationTypes: UIUserNotificationType = [.Badge, .Sound, .Alert]
// let notificationSettings = UIUserNotificationSettings(forTypes: notificationTypes, categories: nil)
// UIApplication.sharedApplication().registerUserNotificationSettings(notificationSettings)

```

### デバイストークン発行または変更ハンドリング

受け取ったデバイストークンをプッシュ通知サーバに通知する処理を行う場所。

```swift
func pushRegistry(_ registry: PKPushRegistry,
    didUpdate credentials: PKPushCredentials,
        forType type: PKPushType)
```

### デバイストークン無効化ハンドリング

デバイストークンが無効になったことをプッシュ通知サーバに通知する処理を行う場所。
…と思われる（呼び出される条件が不明なため）

```swift
optional func pushRegistry(_ registry: PKPushRegistry,
    didInvalidatePushTokenForType type: PKPushType)
```

### VoIPプッシュ通知受信ハンドリング

プッシュ通知を受け取った際の処理を書く場所。

```swift
func pushRegistry(_ registry: PKPushRegistry,
    didReceiveIncomingPushWith payload: PKPushPayload,
        forType type: PKPushType)
```

# VoIPプッシュプログラミングの注意点

## アプリ側

### アプリはVoIPプッシュによって「起動」する場合がある

端的にいうとプッシュ通知受信時にアプリが完全停止(アクティブでもなく、バックグランドにもいない)状態の場合は

```swift
func application( application: UIApplication, didFinishLaunchingWithOptions launchOptions: NSDictionary? ) -> Bool
```

が呼ばれる場合があるということ。
このとき `application.applicationState == UIApplicationStateBackground` となっていることを考慮する必要がある。

プッシュ通受信ハンドリング処理(`pushRegistry(_:didReceiveIncomingPushWith:forType:)`)については常に呼ばれるので、通知に対する処理はここに集約できる、が、アプリがバックグランドの場合では可能な処理が限定されるので、多くの場合はアプリ状態を判別して処理を分ける必要を迫られることになる。

#### バックグランドでできることについて

バックグランドではUI操作ができないので、処理の内容としては
・通信処理
・データの保存・読出
・ローカル通知登録(UILocalNotification)
などを行うことになる。

なお、プログラムからアプリをアクティブにすることも(~~プライベートAPIを使わなければ~~)できないようなので、アクティブにするにはローカル通知を登録し、ユーザに知らせてアクティブにしもらうしかなさそう。(iOS 10からはリッチな通知が行えるようなのでだいぶUXが改善される可能性あり)

バックグランドで動作可能な時間も制限されているようなので(時間は不明)、あまり長い処理はできないと考えておいた方が無難。

### VoIPプッシュそのものは通知センターに関知しない

標準プッシュではアプリがアクティブでない場合、通知は通知センターに~~問答無用で~~表示されましたが、VoIPプッシュでは常にアプリ処理となるため、通知センターには何も表示されません。

表示したければ受信にローカル通知(UILocalNotification)を登録する処理が必要。
ローカル通知を出すには標準プッシュ同様、通知を出すことについてユーザーの許可を得る(`registerUserNotificationSettings`メソッド呼び出しが)必要あり。

手間といえば手間なものの、通知の表示有無や通知内容をアプリ側の処理で変えられるのは大きなメリット。

### デバイストークンが変更されるタイミングについて

標準プッシュではデバイストークンが変更されるタイミングは明文化されておらず、OSバージョンの変遷でいろいろと移り変わって開発者を困らせて(？)いました。iOS 9ではアプリアンインストールだけでも変化する。

VoIPプッシュのデバイストークンのについても変更タイミング明文化されていないので、いつ変わっても良いような考慮が必要。ただ2016/6/10検証時点ではiOS 9ではアプリアンインストールでは変化しなかった。

## サーバ側

### プロバイダーAPIは従来通りのもの(Binary Provider API)でもが今のところ使えるが…

証明書はVoIPのものでないとダメ。（p12ファイル→pemファイルへの変換等は標準プッシュの時と同じ)

従来通りのもの(Binary Provider API)はいつまで使えるのか不明なので、新しいAPNs Provider API (HTTP/2)に対応する準備をした方が無難。

### 証明書の管理について

標準プッシュで必要な証明書を得るためと作った秘密鍵を使ってVoIP用の証明書を取ることもできるがが、キーチェーンアクセスからの書き出しに際してトラブルの元になるので別々にしておいたほうが無難。

※特に外部サービス(Amazon SNSなど)に秘密鍵＆証明書ファイル(p12)を登録する際には、エントリが複数あるとうまく登録できない場合あり


## 共通

### 標準プッシュとの共存について

標準プッシュとの共存は技術的な意味では可能。

ただし、デバイストークンを標準プッシュのものと、VoIPプッシュのものの２つを適切に取り扱う必要あり。
アプリ側では、VoIPプッシュ受信でローカル通知を出すとなると

- 標準プッシュ(リモート通知)
- ローカル通知
- VoIPプッシュ(PushKit)

を管理することになるので、アプリ側では注意してクラス設計を行わないとAppDelegateがカオスなこととなる。アプリ状態(Active/Inactive/Background)の違いにも気を使ってテストを行わないとバグの温床となる気配が漂う。

サーバ側はとしては、APNsの呼び出し方自体は標準プッシュとVoIPプッシュで同じなものの、デバイストークンの種類

- 標準プッシュ(sandbox)
- 標準プッシュ(production)
- VoIPプッシュ(sandbox)
- VoIPプッシュ(production)

を管理することと、接続時の証明書を使い分け、通知ペイロードを作り分ける、などが必要となる。

# 参考
## 標準プッシュ
- [Apple Push Notification Service]
(https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html)
- [APNs Provider API]
(https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/APNsProviderAPI.html)
- [Binary Provider API]
(https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Appendixes/BinaryProviderAPI.html)
- [The Remote Notification Payload]
(https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/TheNotificationPayload.html)
- [Local および Push Notification プログラミングガイド (PDF)]
(https://developer.apple.com/jp/documentation/RemoteNotificationsPG.pdf)

## VoIPプッシュ
- [Voice Over IP (VoIP) Best Practices]
(https://developer.apple.com/library/ios/documentation/Performance/Conceptual/EnergyGuide-iOS/OptimizeVoIP.html)
- [Introduction to PushKit]
(https://developer.apple.com/library/ios/documentation/NetworkingInternet/Reference/PushKit_Framework/)