1. Qiita
  2. 投稿
  3. iOS

iOSのPUSH通知(APNS)の特徴・ノウハウまとめ(iOS 9まで対応)

  • 1276
    いいね
  • 0
    コメント

今開発しているPlayer!で、チャット系のリアルタイム更新処理が必要となったので、色々調べながらまとめています。
分量が多いので、とりあえずまずは小出しにAppleのPUSH通知の特徴・ノウハウについてまとめたものを公開します。

→ 「リアルタイム更新処理」全体にフォーカスした記事も書きましたヽ(・ω・`)
iOS - チャットなどリアルタイム更新が必要なスマフォアプリの構成について考えてみた - Qiita

アプリが終了状態になっていてもサーバーから通知出来る唯一の手段

まず当たり前のことからですが、最大の特長だと思います。
この理由によって、双方向通信などを併用するにしてもPUSH通知対応は必須です。
(サーバー経由でなければ、位置情報トリガーなど他にもいくつか終了状態から起こす方法は存在します。)

ユーザーにPUSH通知を不許可にされたら届かない

さらに、初回の確認で不許可にされたら、設定画面でオンにしてもらうしかありません。
なので、初回の確認チャンスは大事にしましょう。

また必要に応じて、すでに不許可にされてしまっているユーザーに対してのケアもしましょう。
どんな画面を出すかはケースバイケースですが、例えばFacebook Messengerは不許可状態でアプリを立ち上げるとこんな強烈な画面が出ます。

IMG_1541.PNG

また、iOS 8では設定画面へ誘導可能なので、アプリ内のボタンタップなどで設定画面へ飛ばしてあげるような配慮もしておきましょう。

let url = NSURL(string: UIApplicationOpenSettingsURLString)!
UIApplication.sharedApplication().openURL(url)

iOS8で復活した設定画面へのURLスキーム - Qiita

現在、許可しているかどうかはこれでチェック出来ます。

func isEnabled() -> Bool {
    return (UIApplication.sharedApplication().currentUserNotificationSettings().types & UIUserNotificationType.Alert) != nil
}

通知ペイロードについて

通知ペイロード長に制限がある

iOS 7までは、バナーに表示される通知文言 + 必要だったらIDなどのメタデータ程度しか送れませんでしたが、iOS 8以降リッチな内容を含められるようになりました。

iOSバージョン ペイロード長制限 備考
iOS 7まで 256 byte  
iOS 8 2KB > iOS 8では、プッシュペイロード長が256バイトから2キロバイトに拡張されました。 https://developer.apple.com/jp/documentation/RemoteNotificationsPG.pdf
iOS 9 4KB WWDC 2015 – Big changes to Apple push notifications

今まで例えば、通知ペイロードにメタ情報としてcomment_id: 3を付与して、クライアントでそれをキーに別途APIサーバーにGETリクエストしていた場合、実際のcomment内容をペイロードに入れちゃっても良いかもしれません。
以下の恩恵を受けられます。

  • レスポンスタイム短縮
    • 通知後に別途APIサーバーへリクエスト不要なので
  • 通知発行後のAPIサーバーの負荷軽減

ペイロード長制限緩和は、こういう使い方をしても良いというAppleからのメッセージにも見えるのですが、実際にやっているケースありますかね?
僕はiOS 7までの感覚が染みついているのか違和感が多少あるものの、こういう使い方してみたいなと思っています。
【追記】→ 実際に活用していますが良い感じです!

通知ペイロードをstructでラップして扱うと便利

僕はSwiftyJSON/SwiftyJSONと組み合わせて以下のようなstructを定義して扱っています。

struct NotificationPayload {
    let json: JSON
    private var aps: JSON { return json["aps"] }
    var alert: String? { return aps["alert"].string }
    var badge: Int? { return aps["badge"].int }
    var sound: String? { return aps["sound"].string }
    var contentAvailable: Int? { return aps["content-available"].int }
    var newsId: Int32? { return json["news_id"].int32 }
    var notificationId: Int32? { return json["notification_id"].int32 }

    init(userInfo: [NSObject : AnyObject]) {
        json = JSON(userInfo)
    }
}

呼び出し例:

let payload = NotificationPayload(userInfo: userInfo)
if let notificatonId = payload.notificationId {
    // ...
}

APNSはsandbox環境よりproduction環境の方がパフォーマンスが良い

僕はあまり実感したこと無かったですが、APNSはsandbox環境よりproduction環境の方がパフォーマンスが良いとのことです。

sandbox環境で試して、「3秒遅延あるからやはり双方向通信じゃなきゃダメですね」みたいな結論にならないように、テストはproduction環境(AdHoc配信・TestFlight配信)で行いましょう。

ios - Push Notifications on Apple Sandbox APN Server performing poorly - Stack Overflow

通知が本当に届くかどうかは保証されない

"Important: Because delivery is not guaranteed, you should not depend on the remote-notifications facility for delivering critical data to an application via the payload.

Apple Push Notification Service より

ただ、設定を正しくしていれば、通知許可しているユーザーにはほぼほぼ届きますね。

iOS 7以降ではサイレントPUSH通知が可能

以下の対応をすると、バックグラウンドからPUSH通知経由でアプリが起動が可能となります。

  • 通知ペイロードにcontent-available: 1を含める
  • Background Modes・Remote notificationsの設定をオンにする

Screen_Shot_2015-07-21_at_16_59_37.png

通常はこのハンドラーが呼ばれますが、

optional func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject])

オンにするとこちらが呼ばれます。

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

さらに、通知ペイロードのalert(通常はPUSH通知メッセージを記載)を無指定にすると、サイレント通知になります。

  • アプリを閉じている状態で通知が来てもバナーなど出ない
  • ただ、通知自体は届いてアプリが起こされるので、そこから処理を走らせられる

変更をユーザーに気づかせる必要は無いけどデータの更新はしておきたい、という時などに活用しましょう。

completionHandlerを30秒以内のなるべく早いタイミングで呼ぶ必要がある

こんな記載があります。

As soon as you finish processing the notification, you must call the block in the handler parameter or your app will be terminated. Your app has up to 30 seconds of wall-clock time to process the notification and call the specified completion handler block. In practice, you should call the handler block as soon as you are done processing the notification. The system tracks the elapsed time, power usage, and data costs for your app’s background downloads. Apps that use significant amounts of power when processing push notifications may not always be woken up early to process future notifications.

- application:didReceiveRemoteNotification:fetchCompletionHandler: - UIApplicationDelegate Protocol Reference

completionHandlerが呼ばれるまでの時間やそれに要したバッテリーなどのコストをモニターして、それに応じて通知を間引くことがある、とのことです。
(頻度が多すぎても間引くという記述をどこかで見た気もしますが、見つかりません。)

つまり、以下に気をつけましょう。

  • あまり重い処理をしない
  • 処理が終わり次第、completionHandlerを速やかに呼ぶ
    • max 30秒(正確にはbackgroundTimeRemainingで取れる値)で、それを過ぎるとアプリが落とされるが、それ以内でも早いに越したことはない
  • 不必要に呼び出しすぎない

では、チャットのメッセージ更新トリガーなどに使うと良くない?

呼び出しすぎて通知が届かなくなったらと、とても心配になる制約です(´・︵・`)
ドキュメントにもっと詳しく条件など明記してあれば嬉しいのですが、無さそうです。

これを見る限り、間引かれるような状況になっても、アプリがフォアグラウンドであったり充電中であれば問題無さそうです。

The silent push messages only seem to be received if the device is charging (ie cable connected) and/or if my app is foreground.

ios - Silent push notifications only delivered if device is charging and/or app is foreground - Stack Overflow

なので、アプリ起動中の高頻度なリアルタイム更新目的として使うのはあり、と言えそうです。

バックグランド通知の注意点

バックグラウンド通知はいくつか発動条件があり、デバッグ時には注意が必要です。

  • 上記の間引きなどがある
  • ホームボタンダブルクリックして手動でアプリを落とした後は届かない
    • もうそのアプリは動作してほしくないというユーザーの意思表示を反映
    • たくさんアプリを起動するなどして自動的にアプリが終了された場合は、有効
    • 開発時はデバッグ実行したあと終了すれば「自動的にアプリが終了された」状態と同等になるので確認簡単

サイレント通知とローカル通知を組み合わせた応用テクニック

以下のようにすると、ユーザーがアプリを開いた瞬間にデータがリフレッシュされている状態を作り出せます。

  1. サイレント通知を発行
  2. 受け取ったクライアントはまだユーザーに何も知らせず、こっそりデータを更新
  3. データ更新が終わったタイミングでローカル通知を発行してユーザーに知らせる

PodcastアプリのOvercastも似たようなことをやっているようです。
(Podcastは容量が大きいので直接処理せずにNSURLSessionのBackground transfersを噛ませていると推測)

  1. 新しいPodcastが配信されたらサイレント通知を発行
  2. 受け取ったクライアントはまだユーザーに何も知らせず、こっそりPodcastダウンロード処理をNSURLSessionのBackground transfersにて開始してcompletionHandlerを呼ぶ
  3. Background transfersが終わったタイミングで、ローカル通知を発行してユーザーに知らせる

新着通知来たらすでにそのPodcastがダウンロードされていて聴ける状態なんてとても快適ですよね( ´・‿・`)

fastlane/PEMで簡単にPUSH通知の証明書作成

PUSH通知の証明書作成周りは色々面倒かつハマりやすいところですが、fastlane/PEMを使うと、pemコマンド一発で出来ます。
App IDとの紐付けまで自動でしてくれます。
ローカルにpemファイル・p12ファイルが生成されるので、あとはそれを用いてGrocerParseなどで使う証明書として指定するだけで済みます。

PUSH通知サービス

全て自前実装でも出来ますが、外部サービスに頼るとけっこう楽になります。

デバッグテクニック

acoomans/SimulatorRemoteNotificationsを使うと、PUSH通知の届かないシミュレーターでもPUSH通知のハンドラーが呼ばれて疑似的に確認出来るようになって便利です。
あくまで疑似的なものなのでPUSH通知自体のテストには使えないですが、PUSH通知のハンドラーが呼ばれた後の制御部分の実装に使うと捗ります。

iOS 8について

iOS 8以降ではアクションを選べる

僕は愛用しているものの、iPhoneだと気づきにくくて実際使っているユーザー少ないかもしれません。
ただ、Apple Watchだと選択肢が隠れて無くて良い感じです。

iOS 8から通知の登録方法が変わった

iOS 9について

けっこうアップデートがあります。
ドキュメント: What's New in Notifications - WWDC 2015 - Videos - Apple Developer(https://developer.apple.com/library/prerelease/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Introduction.html#//apple_ref/doc/uid/TP40008194)

バナー上で直接テキスト入力が可能になる

AppleのMessagesアプリの場合はこれまでも出来ました、それが一般のアプリにも開放されました。

Screen Shot 2015-07-21 at 18.16.31.png

Device tokenサイズが大きくなる( iOS 9リリースタイミングでは無いです )

これは重要だと思います。
Device tokenのサイズがこれまでの32 bytesから100 bytesになります。
ただ、これは来年に予定されていることで、iOS 9のタイミングでは無いようです。

Large Device Tokens / Coming in 2016 (WWDC 2015資料 p. 97より)

ちなみに僕はこのように変換した文字列をサーバーに送っています。

let tokenId = deviceToken.map { String($0, radix: 16) }.joined()

参考: Swift 3.0でのプッシュ通知用のDevice TokenのData型からString型への変換方法 - Qiita

iOS 8まではtokenIdは64文字となるためもしサーバーのDB定義を64文字固定としていたら、来年の変更タイミングあたりまでに(200文字まで許容に?)対応が必要となるはずです。

Providers API

【追記】APNs Provider APIというドキュメントが追加されました!

今までは、こういう構成で、APNSサーバーがが通知に成功したかどうかを、APNS Feedbackサーバーに一々問い合わせしなくてはいけないようになっていて、おそらくこの成功確認はしてないケースがほとんどだと思います。

Screen Shot 2015-07-21 at 18.20.31.png

それがHTTP/2ベースに変わって、直接結果を受け取れる自然な構成に変わります。

Screen Shot 2015-07-21 at 18.20.49.png

Screen Shot 2015-07-21 at 18.21.09.png

しばらくはこれまでの仕組みも使えるので、HTTP/2ベースの恩恵を得る目的など無ければ、とりあえず対応不要です。

証明書が1つになる

今までdevelopment・productionなどそれぞれ証明書を用意しなくてはならず、生成・管理が面倒でしたが、1つになります( ´・‿・`)