この記事は Todd Kerpelman、デベロッパー アドボケートによる The Firebase Blog の記事 "Debugging Firebase Cloud Messaging on iOS" を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。


Todd Kerpleman


Todd Kerpelman
Developer Advocate
Firebase Cloud Messaging のデバッグは、iOS での Firebase の利用に関する StackOverflow の質問の中でもっともよく目にするものの 1 つです。そのため、できるだけ StackOverflow のポイントを稼ぐために(そしてもちろんデベロッパー コミュニティの成長のために)、Firebase Cloud Messaging(FCM)が iOS 端末でうまく動作しない場合にどうすればよいか、完全なデバッグガイドをまとめておきたいと思います。まず、少しばかり時間をとって、iOS での FCM を理解するための動画(英語)を見てみることをお勧めします。FCM の内部で何が行われているかがわかりますので、この点を知っておくとデバッグに役立ちます。では、どうぞ。お待ちしています。



見終わりましたか?動画の説明から、複数のシステムが相互通信を行っていることがわかったのではないかと思います。
  1. アプリサーバー(または Firebase Notifications)が Firebase Cloud Messaging と通信
  2. 次に、Firebase Cloud Messaging が APNs と通信
  3. APNs がユーザーの対象端末と通信
  4. ユーザーの対象端末上で、iOS がアプリと通信

これら 4 つの通信パスがあるということは、うまく動作しない可能性のある箇所が 4 つあるということです。そのため、「通知は送信されたと言っているのに、端末には何も表示されない」という事態が発生する場合、解決するために詳しい調査が必要になります。ここでは、こういったエラーを調査する際にお勧めの手順を紹介しましょう。

1. すべての connectToFCM() の呼び出しを一時的に無効にしてみる

先ほどの動画では、アプリがフォアグラウンドにある場合、connectToFCM() を呼び出して明示的に Firebase Cloud Messaging に接続できることが紹介されていました。この方法を使うと、アプリは content-available フラグがないデータのみのメッセージを直接 FCM から受信できます。
この機能は状況によっては便利かもしれませんが、デバッグ中は無効にしておくことをお勧めします。これは、単に 1 つの余分な要素を排除するためです。私は何回か、「フォアグラウンドでは通知を受信できるが、バックグラウンドではできない」という問題を目にしてきました。これはおそらく、実際には APNs の設定がうまくいっておらず、FCM チャンネル経由でのみメッセージを受信しているために発生しています。
この時点でうまく動かない場合: 元々の「フォアグラウンドでは通知を受信できた」状態から「通知が一切届かなくなった」という状態になった場合、最初からアプリで APNs から通知を受信できるように正しく設定されていなかったという印です。そのため、アプリは以前よりもさらに動かなくなっているかもしれませんが、少なくとも現在は一貫して動かなくなっているはずです(よかったですね!)。APNs の実装をデバッグするには、引き続き以降をお読みください。
次のいくつかのステップでは、「Notifications、FCM、APNs、iOS、アプリ」という連鎖を逆方向にたどってゆきます。ではまず、iOS が実際にアプリと通信できているかを確認するところから始めましょう。

2. 懐かしの print() デバッグを追加してみる

Firebase Cloud Messaging は、メソッド スウィズリング(メソッドの入れ替え)という賢い方法を使って、AppDelegate で application(_:didRegisterForRemoteNotificationsWithDeviceToken:)application(_:didFailToRegisterForRemoteNotificationsWithError:) をまったく実装しなくてもよい仕組みを実現しています。
しかし、デバッグをする際には、気づくべきエラーが起こっていないかを確認するために、これらのメソッドを追加して何らかのデバッグ情報を表示したい場合があります。まずは、失敗した場合に呼ばれるメソッドにいくつかのデバッグ出力を追加してみましょう。たとえば、次のようなものです。

func application(_ application: UIApplication,
    didFailToRegisterForRemoteNotificationsWithError error: Error) {
  print("Oh no! Failed to register for remote notifications with error \(error)")
}
理論上、ここで何らかのエラー メッセージが表示された場合は、FCM クライアント ライブラリからもメッセージが出力されるはずですが、ここではあえて独自のメッセージを追加しましょう。こうすれば、Xcode の出力で特定の文字列(たとえば、先ほどの例の「Oh no!」)を検索できるためです。また、こうしておくと、その行にブレークポイントを貼れるので便利です。
さらに、didRegister... メソッドで端末トークンを人間が読める形で表示するようにしてみましょう。
func application(_ application: UIApplication,
    didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
  var readableToken: String = ""
  for i in 0..<deviceToken.count {
    readableToken += String(format: "%02.2hhx", deviceToken[i] as CVarArg)
  }
  print("Received an APNs device token: \(readableToken)")
}
デバッグ メソッドを追加するために、メソッド スウィズリングを無効にしたりする必要はありません。Firebase は、独自のメソッドを呼び出した後、自動的にこれらのメソッドを呼び出してくれます。
この時点でエラー メッセージが表示された場合: エラー メッセージが表示された場合や、端末トークンが返されなかった場合は、エラー メッセージを確認すると、何がうまくいっていないかを突き止めるよい手がかりが得られます。この時点でよく起こるのは、「動かなかった理由を他の人に説明したくない」タイプの失敗です。たとえば、次のようなものです。
  • 実機でなく、iOS シミュレータでテストしていた
  • Xcode のプロジェクト設定で、プッシュ通知を有効にし忘れていた
  • アプリの起動時に application.registerForRemoteNotifications() を呼び出していなかった
お分かりだと思いますが、これらは単純なミスです。しかし、Xcode のコンソールにメッセージを表示しなければ、気づかないままになることも多いでしょう。

3. ユーザーに表示される通知を送信できることを確認する

iOS アプリで通知のアラートを表示したり通知音を鳴らしたりするには、明示的にユーザーのパーミッションを取得する必要があります。アプリでバックグラウンドの通知メッセージを受信できていないように見える場合は、単にアプリに iOS のパーミッションがないだけの可能性があります。
iOS 10 以上の場合、アプリのどこかに次のコードを追加するとこの点を確認できます。
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
  print("Alert setting is \(settings.alertSetting ==
    UNNotificationSetting.enabled ? "enabled" : "disabled")")
  print("Sound setting is \(settings.soundSetting ==
    UNNotificationSetting.enabled ? "enabled" : "disabled")")
}
この時点で「disabled」メッセージが表示された場合: アプリが通知を出すためのパーミッションを意図せずに拒否してしまったか、そもそもパーミッションを取得しようとしていなかったかのどちらかが原因です。
アプリが通知を出すためのパーミッションを尋ねてきたときに、意図せず「許可しない」ボタンをタップしてしまった場合は、[設定] を開いて自分のアプリを探し、[通知] をクリックして [通知を許可] スイッチを切り替えると、問題を修正できます。

                                   

一方で、ユーザーに表示される通知を出すためのパーミッションを要求していなかった場合は、アプリのどこかに次のようなコード(iOS 10 以上)を追加する必要があるということです。
let authOptions : UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(options: authOptions)
  { (granted, error) in
    if (error != nil) {
      print("I received the following error: \(error)")
    } else if (granted) {
      print ("Authorization was granted!")
    } else {
      print ("Authorization was not granted. :(")
    }
  }
この時点で何の問題もないように見える場合は、APNs 接続のデバッグに進みましょう。

4. 直接 APNs を呼び出してみる

通知の処理に FCM を使っていても、直接 APNs を使うことは可能です。これはいくつかの方法で試すことができます。その 1 つは、NWPusher のようなオープンソース ツールを使ってテスト通知を送信してみることです。しかし、個人的には、curl を呼び出して直接 APNs リクエストを送信する方がいいと思っています。
現在の APNs は HTTP/2 をサポートしているので、APNs に curl リクエストを行う方が簡単です。しかし、そのためには、curl が最新バージョンになっている必要があります。最新かどうかは、curl --version を実行すると確認できます。おそらく、次のように表示されるはずです。
curl 7.47.1 (x86_64-apple-darwin15.6.0) libcurl/7.47.1 OpenSSL/1.0.2f zlib/1.2.5 nghttp2/1.8.0
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp smb smbs smtp smtps telnet tftp
Features: IPv6 Largefile NTLM NTLM_WB SSL libz TLS-SRP HTTP2 UnixSockets
APNs と通信するには、7.43 以上のバージョンの curl が必要で、Features に HTTP2 が含まれている必要があります。curl のバージョンがこの要件を満たさない場合は、アップデートする必要があります。Simone Carletti 氏によるこちらのブログ投稿には、その手順が丁寧に示されています。
次に、Apple Developer Portal からダウンロードした .p12 ファイルを .pem ファイルに変換します。これを行うには、次のコマンドを使用します。
openssl pkcs12 -in MyApp_APNS_Certificate.p12 -out myapp-push-cert.pem -nodes -clcerts
さらに、対象端末の APNs 端末トークンも必要です。先ほどの application(_:didRegisterForRemoteNotificationsWithDeviceToken:) メソッドに掲載したデバッグ テキストを追加している場合は、Xcode のコンソールから端末トークンを取得できます。これは、ab8293ad24537c838539ba23457183bfed334193518edf258385266422013ac0d のように表示されています。
これで、curl を呼び出すことができます。次の例をご覧ください。
> curl --http2 --cert ./myapp-push-cert.pem \
-H "apns-topic: com.example.yourapp.bundleID" \
-d '{"aps":{"alert":"Hello from APNs!","sound":"default"}}' \
https://api.development.push.apple.com/3/device/ab8293ad24537c838539ba23457183bfed334193518edf258385266422013ac0d
ここでは、3 つの点に注意します。
  1. --cert 引数には、先ほどの手順で作成した .pem ファイルを指定します。
  2. apns-topic には、アプリのバンドル ID を指定してください。なお、apns-topic の概念は、Firebase Cloud Messaging のトピックとはまったく異なる概念なので、ご注意ください。
  3. URL の最後には、忘れずに端末トークンを含めるようにします。上の例をコピーして貼り付けるだけでは動作しません。
うまく動作すれば、端末にプッシュ通知が表示されるはずです。その場合は、次のステップに進んでください。動作しない場合、以下の点を確認します。
  1. APNs から何らかのエラー メッセージが返されましたか?その場合は、何かがうまくいっていないという印です。よく見られるのは、次のようなメッセージです。
    1. 「Bad device token」 -- メッセージにあるとおり、端末トークンが間違っています。アプリから正しくコピーできているかどうかもう一度確認してください。
    2. 「Device token not for topic」 -- アプリのバンドル ID にトピックが正しく設定されていない可能性があります。または、正しい証明書を使っていない可能性もあります。私の経験では、間違った .pem ファイルを使ったとき、このメッセージが表示されたことがあります。
  2. アプリはバックグラウンドで動作していますか?アプリがフォアグラウンドで動作している場合、iOS は自動的に通知アラートを表示したり、通知音を鳴らしたりしません。
    1. ただし、iOS 10 では、アプリがフォアグラウンドで動作しているときも、かなり簡単にアラートを表示できるようになっています。userNotificationCenter(_:willPresent:withCompletionHandler:) の最後で completionHandler([.alert]) を呼ぶだけで、これを行うことができます。
  3. 有効な APNs リクエストを送信していますか?いくつかのリクエストは、構文的に正しくても拒否される可能性があります。この投稿の執筆時点では、content-available フラグを含まないサイレント通知の送信や、優先度 high のサイレント通知の送信などがこれにあたります。
    1. さらに、サイレント通知を受信したアプリが妥当な時間内に completionHandler を呼び出さない場合や、通知の処理に電池を使いすぎる場合、iOS によってサイレント通知が制限される場合があります。詳しくは、Apple のドキュメントをご覧ください。
  4. APNs に問題が起きていませんか?念のため、APNs と APNs Sandbox のステータスを https://developer.apple.com/system-status/ で確認してください。
うまく動作していると思われる場合は、次のステップに進みます。

5. curl で直接 FCM を呼び出してみる

APNs の呼び出しがうまく動作していることが確認できたら、次のステップでプロセスの FCM 部分が動作していることを確認します。そのために、もう一度 curl 呼び出しを行います。これを動作させるには、サーバーキーと、対象端末の FCM 端末トークンの 2 つが必要です。
サーバーキーを取得するには、Firebase コンソールのプロジェクトで Cloud Messaging の設定を開きます。175 文字の長い文字列で表示されているのがサーバーキーです。



FCM 端末トークンの取得には、少しばかり作業が必要です。アプリは、最初に APNs トークンを受信したとき、それを FCM サーバーに送信し、それと引き替えに FCM 端末トークンを受け取ります。この FCM トークンが戻されると、FCM ライブラリは「インスタンス ID トークン更新」通知を行います。 1
この通知(firInstanceIDTokenRefresh の NSNotification)を受け取ることができれば、FCM 端末トークンを確認できますが、この通知は端末トークンが変更された場合にしか発行されません。これは頻繁に起こるものではなく、デバッグビルドから本番ビルドに切り替えたときや、初めてアプリを実行したときのみ行われます。それ以外の場合、この通知は発生しません。
ただし、キャッシュされた FCM 端末トークンを取得することは可能です。InstanceID ライブラリを使うと、保存されているあらゆる端末トークンを取得できます。ここでは、最新の FCM トークンを取得してみます。次のようなコードを書いてみましょう。
  func application(_ application: UIApplication, didFinishLaunchingWithOptions
    // ...
    printFCMToken() // This will be nil the first time, but it will give you a value on most subsequent runs
    NotificationCenter.default.addObserver(self, 
      selector: #selector(tokenRefreshNotification), 
      name: NSNotification.Name.firInstanceIDTokenRefresh, 
      object: nil)
    application.registerForRemoteNotifications()
    //...
  }

  func printFCMToken() {
    if let token = FIRInstanceID.instanceID().token() {
      print("Your FCM token is \(token)")
    } else {
      print("You don't yet have an FCM token.")
    }
  }

  func tokenRefreshNotification(_ notification: NSNotification?) {
    if let updatedToken = FIRInstanceID.instanceID().token() {
      printFCMToken()
      // Do other work here like sending the FCM token to your server
    } else {
      print("We don't have an FCM token yet")
    }
  }
アプリを初めて実行すると、FCM トークンがないというメッセージが表示され、そのすぐ後に実際のトークンを含むメッセージが表示されます。2 回目以降の実行では、キャッシュされたトークンがすぐに表示されます。これは 153 文字のランダムな文字列です。サーバーキーとよく似ていますので、混同しないようにしてください。
これで必要な情報がそろったので、curl を呼び出せるようになります。次のような呼び出しを行ってみてください。
> curl --header "Content-Type: application/json" \
--header "Authorization: key=AU...the rest of your server key...s38txvmxME-W1N4" \
https://fcm.googleapis.com/fcm/send \
-d '{"notification": {"body": "Hello from curl via FCM!", "sound": "default"},
"priority": "high",
"to": "gJHcrfzW2Y:APA91...the rest of your FCM token...-JgS70Jm"}'
うまくいけば、端末に通知が表示され、FCM から「Success」というレスポンスを受け取ります。
{"multicast_id":86655058283942579,"success":1,"failure":0,"canonical_ids":0,"results":[{"message_id":"0:1486683492595106961%9e7ad9838bdea651f9"}]}
成功というレスポンスがあっても、喜びすぎてはいけません。これでわかるのは、FCM がメッセージを正常に受け取ったというだけです。FCM が APNs にメッセージをうまく送信できたとは限りません。本当に必要なのは、端末上の通知です。
通知をうまく受け取れていないと思われる場合、次の点を確認します。
  • レスポンスにエラー メッセージが表示されていますか?それを無視してはいけません。こういったメッセージは、何が起きているのかを理解するための大きなヒントになります。
    • InvalidRegistration が表示された場合、FCM 端末トークン(実際は「registration token」と呼ばれています)が正しくありません。
    • 「The request's Authentification (Server-) Key contained an invalid or malformed FCM-Token」というメッセージとともに 401 エラーが返された場合、おそらくサーバーキーが正しくありません。文字列全体を Firebase コンソールから正しくコピーしていることを確認してください。
  • priorityhigh に設定していますか?Android 端末と iOS 端末では、優先度 medium と high の解釈が異なります。
  • Android では、優先度 medium は「メッセージは送るが、ユーザーの端末が Doze モードになっている場合、それを考慮する」という意味になります。これは、優先度を指定しなかった場合、FCM はデフォルトの優先度「medium」を使用するためです。
    • iOS では、優先度 medium(または 5)が意味するのは、せいぜい「そのうち通知するかもしれない。でも、このおかしな世界では、確実なことなど何も言えないよ ¯\_(ツ)_/¯」といった感じです。
    • これは、優先度を指定しなかった場合、APNs のデフォルトの優先度が 10(または「high」)になるためです。また、優先度 medium でメッセージを送るよう求められるのは、データのみの content-available メッセージを送る場合だけです。
    • ほとんどのユーザーに表示されるメッセージは、Android 端末では優先度 medium で、iOS 端末では優先度 high で送るのが理想的です。Firebase Notifications の通知パネルを使うと、とても簡単にこれを行えます。
  • FCM 構文ではなく APNs 構文を使っていませんか?FCM は、FCM の言葉を適切に APNs の言葉に変換するようになっているものの、最初から APNs 構文を送信すると、FCM はそれを正しく認識できません。そのため、FCM 向けの正しいフォーマットでメッセージを送信しているかを再度確認してください。特に、「priority」が「10」でなく「high」に設定されていることを確認します。
    • content-available メッセージを送信する場合、"content-available": ではなく、アンダースコアを使って "content_available": true を指定していることを確認します。2
    • この時点で、Firebase Notifications の通知パネルを使って通知を送信してみるとよいでしょう。Notifications 経由で呼び出すことができ、curl では呼び出せない場合、メッセージが正しくフォーマットされていない印かもしれません。
  • Firebase コンソールに APNs 証明書をアップロードしましたか?その有効期限が切れていませんか?FCM が APNs と通信するためには、証明書が必要です。
6. Firebase Notifications の画面やサーバーから呼び出してみる

ここまできた場合、基本的に FCM、APNs、iOS、アプリというパスが確立されており、うまく動作しています。そのため、この時点で Firebase Notifications 画面から送られる通知がうまく動作しないのであれば、それは驚くべきことです。その場合、status.firebase.google.com を確認し、Cloud Messaging サービス(Notifications も含まれます)に何らかの問題が発生していないかを調べてみるとよいでしょう。
サーバーのコードに問題があるなら、それはお使いのサーバーで対応する必要があります。しかし、正しい FCM 呼び出しを行うために生成する必要があるデータについて正確に理解できたと思いますので、この部分は皆さん自身で十分解決できるでしょう。少なくとも、自信があるふりはできるでしょう。ほとんどの人はそれでごまかすことができます。

お疲れ様でした。長い道のりでしたが、ステップ 2 で Xcode のプロジェクトのスイッチを切り替え忘れていた、というようなことに気づいていただけたなら幸いです。そういう方は、おそらくこのまとめの部分は読んでいないでしょう。考えてみれば、ここまで読んでいる皆さんは、おそらくまだ実装上解決されていない問題があるということでしょう。その場合は、サポート チャンネルもご覧ください。または、この時点では、基本的に私はすでにアドバイスできることはありませんので、@lmoroney に質問してください。

お読みいただき、ありがとうございました!

[1] これは APNs 通知ではなく、NSNotification です。用語がややこしいですね。
[2] 1 つの興味深いエラーを紹介しましょう。あるデベロッパーは、どういうわけか、アプリがフォアグラウンドにある場合のみ content-available メッセージを受信できると言っていました。そのデベロッパーは、(ステップ 1 のように)FCM に明示的に接続しており、メッセージに(無効な)"content-available" キーを含めて送信していました。これは有効な APNs の content-available メッセージに変換できないため、FCM は FCM 経由だけで送信されるべきデータのみのメッセージと解釈しました。そのため、アプリがフォアグラウンドにある場合だけ動作していました。


Posted by Khanh LeViet - Developer Relations Team