iOS 18のサイレントプッシュ通知:バックグラウンド動作の仕組みと実装時の注意点

Published on: | Last updated:

iOSのバックグラウンド処理って、なんか色々あって毎年ちょっとずつ変わるから、正直キャッチアップするの大変ですよね。特にiOS 18になって、BGTaskSchedulerみたいな新しいAPIも本格化してきて、「結局どれ使えばいいの?」ってなりがち。

この前、iOS 18のバックグラウンドタスクの基本について書いたんですけど、今回はその続き。特に、よく聞かれるし、よく誤解されてる「サイレントプッシュ通知」(Silent Push Notification)について、今の僕の頭の中を整理する感じで話してみようと思います。これ、ユーザーにバレずに裏でこっそりアプリを動かせる、めっちゃ便利なやつなんですけど、落とし穴も結構多いんで。

今回はベータ版の話じゃなくて、もう安定版のiOS 18を前提にした話です。正直、Appleのドキュメントだけ読んでも「で、実際どうなの?」って部分、多いじゃないですか。なので、そこらへんのリアルな話をしていきます。

重點一句話

サーバー側から「今だ!」っていうタイミングで、アプリを裏でこっそり起こして何かさせたいならサイレントプッシュ通知。でも、100%確実に届くわけじゃないから、それ前提の作りは危ない。これが結論ですね。

そもそも、サイレントプッシュって何者?

たぶん知ってる人も多いと思うけど、一応おさらい。サイレントプッシュ通知っていうのは、その名の通り「音も鳴らないし、バナーも出ない、静かな」プッシュ通知のこと。ユーザーの画面には何ーんにも表示されない。でも、通知が端末に届いたのをきっかけに、スリープしてるアプリが裏でちょっとだけ目を覚まして、処理を実行できる仕組みです。

よくある使い方は、メッセージアプリが新着メッセージをこっそり取ってきたり、ニュースアプリが最新記事のリストを更新したりとか。Apple的には、あんまり頻繁じゃなくて、不定期にコンテンツを更新したいときに使ってね、って言ってますね。

普通の「通知送りますけど、いいですか?」っていう許可ダイアログもいらないのがミソ。ユーザーが知らないうちに、裏でコンテンツを最新に保てる。…まあ、ちゃんと条件が揃えば、ですけど。

サイレントプッシュ通知の簡単な流れ
サイレントプッシュ通知の簡単な流れ

で、BGTaskSchedulerと何が違うの

「裏で動かすならBGTaskSchedulerでいいじゃん」って思う人もいるはず。僕も最初そうでした。この2つ、似てるようで全然役割が違うんですよね。どっちか片方っていうより、どう使い分けるかがポイント。

僕なりの解釈で、ざっくり比較表を作ってみました。

比較ポイント サイレントプッシュ通知 BGTaskScheduler
トリガー(きっかけ) サーバー側。「今すぐ更新して!」って感じで、サーバー主導 アプリ側。「たぶん数時間後にやっといて」ってOSに予約する感じ。アプリ主導
実行タイミング ほぼリアルタイム(なはず)。でも、遅延したり届かなかったりする。不確実 OSの都合次第。端末が充電中でWi-Fiに繋がってるときとか、都合のいい時にまとめて。確実性は高いけど、時間は読めない
得意なこと 「記事が公開された」「返信が来た」みたいな、いつ起こるか分からないイベントを伝えるのに最適。 「毎朝6時にデータを更新」みたいな定期的な処理や、取りこぼした更新をまとめて同期するのに向いてる。
注意点 送りすぎると無視される。ユーザーがアプリを強制終了してると届かない。マジで「届けばラッキー」くらいで考えるべき。 デバッグがちょっと面倒。`e -l po "BGTaskScheduler.swizzleBGTaskScheduler()" `とかやらないといけないし。あと、即時実行はできない。

要するに、リアルタイム性がちょっとでも欲しいならサイレントプッシュ、定期メンテナンス的な役割ならBGTaskScheduler。両方組み合わせるのが一番賢いやり方かな、と。例えば、サイレントプッシュで更新を試みて、BGTaskSchedulerで半日に一回、取りこぼしがないかチェックする、みたいな感じです。

どうやって実装するの?

じゃあ、実際にどうやって作るのか。手順自体はそんなに複雑じゃないです。

Xcode側の設定

まずはプロジェクト設定。Signing & Capabilities タブで「Background Modes」を追加して、その中の「Remote notifications」にチェック。これを忘れると、いくら通知を送ってもアプリが裏で起動してくれません。基本だけど、意外と忘れがち。

XcodeでのBackground Modes設定
XcodeでのBackground Modes設定

あとは普通にプッシュ通知を受け取るための`UIApplication.shared.registerForRemoteNotifications()`を呼ぶだけ。これはサイレントだろうが普通だろうが必要ですね。

APNsペイロードの作り方

ここが一番のポイント。サイレントプッシュにするには、APNsペイロードの`aps`辞書の中に`"content-available": 1`を入れること。これだけです。

逆に、`alert`とか`sound`、`badge`は絶対に入れちゃダメ。もしこれらが入っていると、iOSは「あ、これ普通のユーザーに見せる通知ね」って判断して、サイレントじゃなくなっちゃいます。

{
  "aps": {
    "content-available": 1
  },
  "data": {
    "update-id": "12345",
    "reason": "new-message"
  }
}

あと、APNsに送るときのHTTP/2ヘッダーも大事。`apns-push-type`は`background`に、`apns-priority`は`5`(低優先)に設定します。`priority`を`10`(高優先)にすると、iOS 13以降だとエラーになるんで注意してください。

AppDelegateでの受け取り

通知が届くと、昔ながらのこのメソッドが呼ばれます。

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

  print("サイレントプッシュが来た!裏で何かやるぞ!")

  // ここでデータのダウンロードとか、DBの更新とかをやる
  // ...重い処理は避ける...

  // 処理が終わったら、必ずcompletionHandlerを呼ぶ
  // 新しいデータがあれば .newData
  // データがなければ .noData
  // 失敗したら .failed
  completionHandler(.newData)
}

一番大事なのは、処理が終わったら絶対に`completionHandler`を呼ぶこと。しかも、だいたい30秒以内に。これを呼び忘れたり、処理が長すぎてタイムアウトしたりすると、OSから「こいつ、行儀悪いな…」って思われて、次からサイレントプッシュで起こしてくれなくなる可能性が上がります。マジで。

これだけは知っておきたい落とし穴(失敗談込み)

理论は簡単なんですけど、実際に運用してみると「あれ、届かないぞ?」ってことが頻発するのがサイレントプッシュ。僕もこれで何度もハマりました。

そもそも「届く保証」はない

まず、大前提として知っておくべきこと。Appleはサイレントプッシュの配信を一切保証していません。これはAppleの公式ドキュメントにも書いてあるし、世界中の開発者が口を酸っぱくして言ってることです。システムが「今はバッテリーが少ないからやめとこ」とか、「このユーザー、このアプリ全然使ってないな」って判断すると、普通に通知を破棄します。

感覚的には、1時間に2〜3回以上送ると、だんだん届かなくなる印象。だから、細かい更新のたびに送るんじゃなくて、ある程度まとめて「更新あるよ」って1回だけ送るのが賢いやり方ですね。

ユーザーによる「強制終了」は死刑宣告

これが一番厄介。ユーザーがAppスイッcherからアプリをシュッて上にスワイプして強制終了させた場合、次にユーザー自身がアプリを起動するまで、サイレントプushは一切届かなくなります。OSからすると「ユーザーが『このアプリ動かしたくない』って意思表示した」ってことなんでしょうね。

これ、一般ユーザーは「アプリをちゃんと終了させる良い操作」だと思ってる人が多いから、なかなか難しい問題。「バックグラウンド更新を有効にするには、アプリを強制終了しないでください」って、どこかでそっと教えてあげるしかないかも。

「バックグラウンドApp更新」と「低電力モード」の壁

当然ですが、iOSの設定で「バックグラウンドApp更新」がオフになってると、サイレントプッシュは機能しません。これはシステム全体の設定と、アプリごとの設定、両方あります。

あと、見落としがちなのが「低電力モード」。これがオンになっていると、「バックグラウンドApp更新」がシステム全体で強制的にオフになるので、やっぱり届きません。開発中は気づきにくいけど、ユーザーの端末はいつ低電力モードになるか分からないですからね。届かないのが当たり前、くらいに思っておくのが精神衛生上良いです。

サイレントプッシュがあるかないかで体験はこんなに違う
サイレントプッシュがあるかないかで体験はこんなに違う

じゃあ、どう付き合っていくのが正解?

ここまで聞くと「サイレントプッシュ、使えないやつだな…」って思ったかもしれません。でも、そんなことはなくて、特性を理解して「補助的な役割」として使えば、ものすごく強力な武器になります。

  • クリティカルな更新には絶対使わない: 「これが届かないとサービスが成り立たない」みたいな処理には向いてません。
  • あくまで「あればラッキー」: ユーザー体験を「良くする」ためのもの。これがなくても、アプリがちゃんと動くように設計するのが大事。
  • 起動時のフォールバックを必ず作る: アプリが起動されたときに、「最後の更新からだいぶ時間経ってるな」って判断したら、自分でデータを取得しにいく処理は必須です。サイレントプッシュが失敗してても、これでリカバリーできます。
  • `BGTaskScheduler`と組み合わせる: さっきも言ったけど、これが王道。サイレントプッシュで即時性を狙いつつ、`BGTaskScheduler`で定期的に差分を同期する。これでかなり堅牢なシステムになります。

結局のところ、サイレントプッシュはOSのご機嫌を伺いながら使わせてもらう機能なんですよね。だから、過信は禁物。でも、うまく使えば、ユーザーがアプリを開いた瞬間に最新のコンテンツが表示されて、「お、このアプリ速いな」って思わせることができる。そのための、ちょっとしたスパイスみたいなものなのかもしれないですね。

あなたのアプリでは、サイレントプッシュと`BGTaskScheduler`、どっちがメインの戦略になりそうですか? もしよかったら、理由も一緒にコメントで教えてくれると嬉しいです。

Related to this topic:

Comments

  1. Guest 2025-10-19 Reply
    うちの子ども、最近もっぱらiPhoneのアプリで勉強してるんだけど、サイレントプッシュ通知ってやつをこの前知ってさ。普通の通知だと「はいまたお知らせ来たよー」みたいに表示されて、そのたびに「えっ開かなきゃダメ?」とか思って集中切れるから困るんだよね。でもサイレントなら画面にも何も出ないし、気付かれずにアプリだけ最新状態になってるから結構ありがたい。正直、親の立場としては学校のお知らせとかでも使えるんじゃ?って思ったりする。 でもねぇ、ときどき全然更新されてなかったりして、「これ本当に新しいデータ…?」と戸惑う時あるんだわ。多分なんだけどバッテリー少ない時とか低電力モードが原因なのかなぁ~。便利なんだけどうまく動いてない感じすると不安で、一応手動で確認したくなる自分がいる…。