iOS アプリを初めて App Store に出すときに一番時間を溶かすのは、コードでもアセットでもなく、「Apple に何をどう説明するか」を書く作業でした。Xcode のビルドが通って TestFlight でも問題なく動いているのに、ストア提出ボタンを押したあとから先で何度か止まる。
自社で出している PollenShield は、自宅と出発時刻を勝手に学習して家を出る5分前に1通知だけ鳴らす iOS アプリで、構造上どうしても authorizedAlways の位置情報を要求します。今 App Store に並んでいるバージョンは、申請の都合だけで何度かビルドを差し替えています。配信開始のお知らせは 先日のニュース に書いた通りで、この記事はその裏で踏んだ申請まわりの作業ログです。
閾値設計の話は 前の記事 で書ききったので、ここでは触れません。今回は「Xcode の外側」で詰まったところだけに絞ります。中小チーム(playpark は 2 人)で初めて iOS アプリを通す人が、自分と同じところで止まらないように。
この記事で扱うこと
- 位置情報を常時取得するアプリで、
UIBackgroundModesの宣言をうっかり広く取りすぎてリジェクトされた話 Always権限のNSLocation*UsageDescription文言を、ユーザーと審査官の両方に向けて書き分ける- App Privacy(プライバシーラベル)を「端末内完結」で申告する時に書きづらかった項目
- アップデートで位置情報ロジックを差し替えた時に、Review Notes をリポジトリで管理して説明を再利用する
- サポート URL・プライバシーポリシー・スクリーンショットの「最低限ここまで」
- TestFlight で自分の足を使ったドッグフードが、審査用デモ動画の素材にもなる
書いていないのは、IAP・Sign in with Apple・年齢制限・Family Sharing まわり。PollenShield は今のところ無料・課金なし・ログインなしで、これらの審査論点には触れずに済んでいます(将来シーズンパスを入れた時に別途痛い目に遭う想定はしている)。
全体像を先に出しておきます。位置情報を常時取得するアプリで、Xcode の外側で書く必要がある文書はだいたい次の 5 ブロックに分かれます。
| # | ブロック | 提出ボタンを押す前に揃えておくこと |
|---|---|---|
| 1 | Info.plist | UIBackgroundModes は使う API ぶんだけ宣言/Always 説明文に常時必要な理由を 1 行/ITSAppUsesNonExemptEncryption: false を宣言 |
| 2 | App Privacy ★ | 端末内完結なら Data Not Collected / Tracking なしを明示 |
| 3 | プライバシーポリシー | iOS アプリ章を追加/Info.plist と同じ言葉で書く |
| 4 | Review Notes ★ | リポジトリで版管理/変えた点と変えてない点を同密度で書く |
| 5 | 提出物アセット | サポート URL /スクショ 3 サイズ/審査用デモ動画素材 |
★ 印が、後ろ倒しにすると一番痛い領域でした。以下、それぞれを順に書きます。
「位置情報のバックグラウンドモード」をうっかり広く取って、Guideline 2.5.4 で止まる
最初のリジェクトはここでした。UIBackgroundModes に location を入れて提出したら、Apple から Guideline 2.5.4 で「persistent location 機能が見当たらないのに location バックグラウンドモードを宣言している」と指摘されて返ってきた。
PollenShield が使う位置 API は次の3つです。
- Significant Location Change(SLC)
- Visit Monitoring(
startMonitoringVisits) - Region Monitoring(
CLCircularRegion)
このどれも、OS 側がイベントを発火してアプリを起こすタイプの API で、こちらが連続的に GPS を回しているわけではない。つまり「persistent location」ではない。なのに Info.plist の背景モード一覧に location を入れていたので、「機能と宣言が合っていない」という見方をされる、というのが Apple の言い分でした。
修正は project.yml の UIBackgroundModes から location を一行落とすだけ。
UIBackgroundModes:
- fetch
- processing
fetch と processing は BGAppRefreshTask / BGProcessingTask のために必要なので残します。位置情報まわりは「OS 主導でアプリが起こされる」設計なので、背景モード宣言なしで動く。ここを最初から正確に取れていたら、リジェクト1回ぶんの往復(提出→審査待ち→指摘→直して再提出→再審査)を踏まずに済んだ、というのが教訓です。
地味なポイントとして、この手の修正でも CFBundleVersion(ビルド番号)は必ず上げないと再アップロードできません。MARKETING_VERSION は据え置きで CURRENT_PROJECT_VERSION: "2" のようにビルド番号だけ進める運用にしています。提出のたびにここを忘れて Transporter で弾かれるのは、たぶん全 iOS 開発者が一度はやる(私もやった)。
Always 権限の説明文は「ユーザーへの言葉」と「審査官への言葉」の両方を兼ねる
authorizedAlways を要求するアプリで一番気を使うのが、Info.plist の説明文 2 種類です。
NSLocationWhenInUseUsageDescriptionNSLocationAlwaysAndWhenInUseUsageDescription
ここに書いた文章は、初回起動時のシステムダイアログにそのまま出ます。同時に、審査官は 「なぜ Always が必要か」をこの文言で読みます。短すぎると審査側で「Always を要求する正当性が読み取れない」と判断されかねないし、長すぎるとダイアログが切れて読みづらい。最終的に PollenShield は次の2行に落ち着きました。
NSLocationWhenInUseUsageDescription:
"出発時刻を学習して、花粉が多い日の朝に通知するために位置情報を使用します。"
NSLocationAlwaysAndWhenInUseUsageDescription:
"出発時刻を学習して、家を出る直前に花粉通知を届けるために位置情報を使用します。位置情報は端末内でのみ処理され、外部に送信されません。"
Always のほうにだけ「端末内でのみ処理され、外部に送信されません」を入れているのが意図的なポイントです。Always を要求するアプリは、ユーザー側も審査側も「なんで常時必要なんだ、データ抜くんじゃないだろうな」を真っ先に疑います。ここで疑念を 1 文で潰しておくと、後段のプライバシーラベル申告とも整合が取れて、説明が一本化されます。
逆にやってはいけないのが、説明文に「より良い体験のために」みたいなふわっとした言葉だけ書いて Always を要求するパターン。これは審査でほぼ確実に「具体的な利用目的を書け」と返ってきます。PollenShield の場合は、「夜間の自宅滞在を学習する」「家を出た瞬間を検知する」 という、ユーザーがアプリを開いていない時間帯にしか取れない情報を扱う必然性があるので、それを文言で見える化しておく必要がありました。
App Privacy は「端末内完結」を素直に書ける構造を、コードと文言の両側で揃えておく
App Store Connect の「App Privacy」セクションは、データタイプごとに「収集するか / 紐付けるか / トラッキングに使うか」を申告する仕組みです。位置情報を扱うアプリだと、ここの選択を間違えるとあとで全部やり直しになる。
PollenShield は「端末内完結・外部送信なし・トラッキングなし」で運用しているので、Location カテゴリの申告は Data Not Collected に倒しました。この申告を成立させるためには、コード側でも本当に外に送っていないことが前提になります。具体的には、
- 学習結果(自宅座標・出発時刻)は
UserDefaultsに保存する - API リクエストには 緯度経度を 1km グリッド単位に丸めた値しか乗せない(個人を特定する精度では送らない)
- 端末 UUID 等の識別子を発行・送信しない
この設計を最初に決めておかないと、Privacy 申告のチェックボックスを入れる場所と、コードのふるまいが噛み合わなくなります。「位置情報を収集している(External)」を1個でも True にすると、Tracking 用途の有無やリンク有無の質問が芋づるで出てきて、答えるたびに「今のアプリの実装、本当にそうなっている?」を再確認することになる。
合わせて、自社サイトのプライバシーポリシーにも iOS アプリの章を増やしました。ストア提出フォームに Privacy Policy URL を入れる欄があるので、リンク先のページに iOS アプリ向けの記述が無いと、審査側で「申告内容とポリシーが合っていない」と見られかねない。playpark の場合は
- 位置情報の利用目的(出発時刻学習・花粉通知)
- 送信先(自社の API のみ、緯度経度は 1km グリッドに丸め)
- 保持期間(端末側のみ、サーバ側は短期キャッシュ)
- トラッキングを行わない旨
を 4 段で書いています。ここはコピペで終わらせず、Info.plist の説明文と表現を揃えておくと、ユーザーも審査官も同じ話を読むことになって余計な疑念が湧きづらい(と信じている)。
位置情報まわりの「3 箇所が同じことを言っているか」は、提出ボタンを押す前のチェックリストとして、頭の中ではこういう三角関係になっています。
3 箇所のうち 1 箇所でも違う言い方になっていると、審査側は必ずそこを突きます。「同じ語彙で 3 回書く」を意識的にやるだけで、申請文書側の指摘はかなり減ります。
アップデートのたびに Review Notes を書き直さないために、リポジトリで版管理する
PollenShield は v1.0 配信後すぐに v1.1 を出していて、これは自宅学習ロジックを SLC 単独から Visit / Region Monitoring 併用に差し替えるアップデートでした。
ここで2回目の申請の山が来ます。「位置情報まわりの内部実装が変わった」 というアップデートは、Apple 側から見ると「権限の使い方が変わったかもしれない」と疑う対象になる。何も書かずに出すと、運が悪ければ「変更内容と権限要求の関係を説明してほしい」で止まる。
なのでバージョンごとに、App Review Information の Notes 欄に貼るテキストをそのままリポジトリで管理しています。
本バージョンは位置情報ロジックの精度改善を含みます。
変更点:
- 自宅学習のアルゴリズム刷新(Significant Location Change のみの入力から、
Visit Monitoring / Region Monitoring を併用する構成へ変更)
- 出発時刻検知の精度向上
- horizontalAccuracy による低精度サンプルのフィルタ追加
用途・ユーザー影響に変更はありません:
- 権限レベル: Always(変更なし)
- 利用目的: 自宅学習に基づく花粉予報通知(変更なし)
- データ取扱い: 端末内完結・外部送信なし(変更なし)
- 新規収集データ項目: なし
Info.plist の各 UsageDescription の文言は前バージョンから変更していません。
App Privacy の申告内容にも変更はありません。
ポイントは「変更したこと」と「変更していないこと」を同じノートの中で同じ密度で書くことでした。書き慣れていないと、つい変更した点だけ熱心に説明して、Apple が一番気にしている「権限と申告は変わってないですよ」を端折りがちになる。前者はソースコード読めば分かる話で、審査官が知りたいのは後者のほう、という配分です。
このノートをリポジトリの docs/ に置いておくと、提出のたびに「前回どう書いたっけ」を Slack 検索する作業が消えるし、社内レビューで「この説明、過剰になっていないか」と二人目の目を通せるのがありがたい。1 人開発でも、未来の自分が他人なので、テキストとして残す側に倒しておくのが結局安い。
サポート URL とスクリーンショットは、提出当日の朝に用意するものではない
申請フォーム側で割と軽く見られがちなのが、Support URL と スクリーンショット です。両方とも「無いと提出ボタンが押せない」枠なのに、後回しにすると当日の朝に LP の /support ページを慌てて作ることになる(私はなった)。
PollenShield では LP のリポジトリ側で /support ページを 1 枚増やして、
- アプリの基本的な使い方
- よくある質問(通知が来ない/自宅判定がおかしい/引っ越したらどうなる)
- 連絡先メールアドレス(プライバシーまわりの問い合わせと同一の窓口)
をまとめています。ここを「ユーザーが本当に問い合わせる時に使えるページ」として書いておくと、審査側も「ちゃんと運用される気のあるアプリだな」と読み取りやすい(と思いたい)。
スクリーンショットも、最低限「アプリが何の役に立つかを 5 秒で理解できる絵」が必要です。PollenShield は機能が「家を出る直前に1通知が出るだけ」で、実は能動的に開く画面が少ない。なので、
- ホーム画面の通知バナー風のモック
- アプリ起動時の今日の花粉量画面
- 設定画面が無い(=何も無い)こと自体を伝える画面
の 3 枚で構成しました。「機能が薄いアプリ」の場合、スクリーンショットで全機能を出し切ってしまうのは戦略的にむしろ正解で、審査側に「これ以上の隠し機能はありません」を見せておくと、追加の確認質問が来づらくなります。
App Store Connect 側で要求される画像サイズは iPhone 6.7 インチ / 6.5 インチ / 5.5 インチの 3 種類が事実上必須なので、デザインデータは 6.7 インチ基準で作って 1.0 倍 / 0.75 倍 / 0.6 倍で書き出すワークフローにしています。Figma の Export スケールを 3 つ用意しておくと、提出のたびに切り出すストレスが消えます。
位置情報アプリの審査用デモ動画は、TestFlight ドッグフードの副産物として作る
authorizedAlways を要求して、しかもバックグラウンドで自宅学習する系のアプリは、審査側から 「動作の様子を見せてほしい」 と来る可能性があります。シミュレータでは Visit / Region のイベントを再現しづらいし、Region Monitoring の OS 発火は実機 + 実生活でしか確認できない。
ここで効くのが、TestFlight で配布した自分のビルドを 1〜2 週間自分で持ち歩いて学習させる期間を、申請プロセスの一部として最初から確保しておくことでした。PollenShield の場合、
- 自分の iPhone で 7 日以上の通勤往復をログ
- DEBUG ビルドで「学習状態デバッグ」画面のスクリーンショットを毎日撮る
- 自宅確定の瞬間 /
didExitRegionの瞬間 / 引っ越し検知の挙動 をデバッグ画面の動画で押さえる
の 3 つを並行してやっています。これをやっておくと、審査官から「動作確認動画を共有してほしい」と来た時に、当日提出可能な素材が手元にある状態になる。Xcode の Simulator では絶対に作れない映像なので、ここを後回しにすると申請フローが 1〜2 週間止まる可能性が普通にあります。
ついでに言うと、デバッグ画面はユーザー向けには出さない(DEBUG ビルド限定)ので、Release ビルドのバイナリには入りません。Apple に動画提出する素材は DEBUG ビルドの動画でも問題なく、「審査官にだけ見せて、ユーザーには見せない」を成立させやすい設計になっています。
Export Compliance とテストアカウント — 最後の小ネタ
ここまで重い話だったので、最後に細かい論点を 2 つ。
Export Compliance: 独自暗号化(HTTPS / TLS 以外)を実装していなければ、Info.plist に ITSAppUsesNonExemptEncryption: false を書いておくと、提出のたびに毎回出てくる暗号関連の質問をスキップできます。これを書き忘れていると、ビルドアップロードのたびに毎回 App Store Connect で「暗号化を使っていますか」のフォームを手で埋めることになる(毎回埋めても答えは同じなので、メタデータ側で1回宣言する側が圧倒的に楽)。
テストアカウント: PollenShield はログイン機能が無いので不要でしたが、ログインありのアプリでは「審査用のテストアカウント情報」を Review Notes に書く欄があります。ここを書き忘れて出すと、審査側がアプリを開けず即返却されます。これも「ログインありの初回提出」だと一度はやらかすやつ、と聞きます(自分は今回回避できたので、また将来 PollenShield Premium を出す時に踏むと思う)。
まとめ — Apple に対する「文書としての説明責任」を、コードと同じ温度で書く
PollenShield を出してみて一番強く感じたのは、App Store 申請のリジェクト要因の半分以上が、コードではなく文書側にあるということでした。Info.plist の説明文・App Privacy の申告・Review Notes・サポートページ・プライバシーポリシー、これらが食い違っていると、ビルドが完璧でも審査で止まる。
まとめるとこうです。
UIBackgroundModesは機能と宣言を一致させる。位置情報 API ごとの「OS 発火 vs 連続取得」の違いを Info.plist 側で正しく書き分けるNSLocation*UsageDescriptionはユーザー向け文言と審査官向け説明を 1 行で兼ねる。Alwaysの必然性を具体的に書く- App Privacy の申告は、コード(送信先・送信精度)と Info.plist 文言と、自社プライバシーポリシーの 3 箇所で同じことを言う
- Review Notes はリポジトリでバージョン管理する。「変えたこと」と「変えていないこと」を同密度で書く
- サポート URL・スクリーンショット・テストアカウント・Export Compliance は提出前日に思い出すと泣く
- 位置情報アプリの審査用動画素材は、TestFlight ドッグフード期間に毎日撮っておくと後で困らない
コードを書く時間と同じくらい、申請文書を書く時間を最初から見積もっておく、というのが今回の一番の学習でした(Xcode を閉じてからが本番、と言ってもいい)。次のアップデートで何かしら申請まわりで詰まったら、また続編として書きます。
- プロダクト LP: pollen-shield.vercel.app
- App Store 配信: PollenShield
「設定は、ゼロ。」の実物は App Store からどうぞ。

