App Store Connect のバリデーションで ITMS-91053 が出たとき、本当に厄介なのは「一度直しても、また出る」ことです。PrivacyInfo.xcprivacy を一枚足してリジェクトを抜けても、次のリリースで便利そうなライブラリを一つ追加した瞬間、別の理由コードで同じ場所に戻されます。私自身、個人開発でいくつものアプリを App Store に出していますが、この往復は最初の数回で本当に時間を溶かしました。アップロードして、半日後にリジェクトメールを受け取り、原因を調べてビルドし直す——この1サイクルが平気で1日を奪います。
ですから本稿は「PrivacyInfo.xcprivacy の書き方」ではなく、ITMS-91053 を二度とアップロード後に踏まないための運用 に振り切ってまとめます。やることは三つです。依存ツリーを棚卸しして対象 API を機械的に特定し、申告を取りこぼしなく集約し、アップロードする前に自分の手元で弾く。この三点を仕組みにしておけば、Rork Max のプロジェクトに新しいライブラリを足しても審査で止まらなくなります。
なぜ「一度直したのにまた出る」のか
Apple の Required Reason API ポリシーは、特定カテゴリの API(UserDefaults・ファイルのタイムスタンプ・空きディスク容量・システム起動時刻など)にアクセスするコードがバイナリに含まれるなら、その使用理由を PrivacyInfo.xcprivacy で申告することを求めます。重要なのは、呼んでいるのが自分のコードでなくても対象になる という点です。Rork Max が書き出すプロジェクトには、ストレージ・通知・解析・課金まわりのライブラリが多数バンドルされ、そのどれかが内部で対象 API に触れていれば申告義務が発生します。
つまり「一度直す」で消えるのは、その時点でバンドルされていたライブラリの分だけです。リリースのたびに依存ツリーは少しずつ変わります。新しいパッケージを足す、既存パッケージのメジャーを上げる、トランジティブ(間接)依存が差し替わる——どれもバイナリに含まれる API の集合を変えます。申告を「最初の提出時に一度書くもの」として扱っている限り、ずれは構造的に再発します。再発を止める唯一の方法は、申告を毎リリース再生成して検証する対象 に格上げすることです。
ステップ1: どの依存が Required Reason API を引いているかを洗い出す
最初にやるのは犯人探しではなく、棚卸しです。Apple が定義する対象カテゴリは内部的に固定の識別子を持っており、ビルド成果物や各ライブラリ同梱の PrivacyInfo.xcprivacy を走査すれば、どのカテゴリが関係するかを機械的に拾えます。Rork Max が書き出した ios/ ディレクトリと node_modules を対象に、次のスクリプトを回します。
#!/usr/bin/env bash
# scan-required-reason.sh
# 依存ツリー内の Privacy Manifest と、対象APIカテゴリ参照を棚卸しする
set -euo pipefail
PROJECT_ROOT = " ${1 :- . } "
echo "== ライブラリ同梱の PrivacyInfo.xcprivacy =="
find " $PROJECT_ROOT /node_modules" -name "PrivacyInfo.xcprivacy" 2> /dev/null \
| while read -r f ; do
pkg = $( echo " $f " | sed -E 's#.*/node_modules/(@[^/]+/[^/]+|[^/]+)/.*#\1#' )
printf '%-45s %s\n' " $pkg " " $f "
done
echo
echo "== ソース/設定に現れる対象APIカテゴリ =="
grep -rhoE "NSPrivacyAccessedAPICategory[A-Za-z]+" \
" $PROJECT_ROOT /ios" " $PROJECT_ROOT /node_modules" 2> /dev/null \
| sort | uniq -c | sort -rn
出力の上段で「自前のマニフェストを持っているライブラリ」が分かり、下段で「プロジェクト全体でどのカテゴリが何回参照されているか」が分かります。ここで頻出するカテゴリこそ、アプリ側マニフェストで確実に申告すべき対象です。Rork Max のプロジェクトでは、@react-native-async-storage/async-storage や react-native-mmkv が UserDefaults を、キャッシュ・同期系のライブラリがファイルのタイムスタンプを引いてくることが多い印象です。
リジェクトメール本文がある場合は、そこに記載された NSPrivacyAccessedAPICategory... を「最低限申告すべきカテゴリの確定リスト」として併用してください。スクリプトの棚卸し結果とメールの両方を突き合わせると、取りこぼしがほぼ無くなります。
ステップ2: 理由コードを正しく割り当てる
カテゴリが分かったら、各カテゴリに実態と一致する 理由コードを割り当てます。ここを実態とずらすとポリシー違反になり、別種の問題に化けます。Rork Max アプリで頻出する4カテゴリと、一般的な用途・理由コードの対応を整理します。
APIカテゴリ
典型的な発生源
一般的な理由コード
意味
UserDefaults
設定・トークン・状態の永続化(AsyncStorage 等)
CA92.1
アプリ自身の機能のための読み書き
FileTimestamp
キャッシュ管理・同期判定・ダウンロード制御
C617.1
アプリ内のファイルへアクセスするため
DiskSpace
ダウンロード前の空き容量チェック
E174.1
書き込み失敗を避けるための残量確認
SystemBootTime
計測・タイマー・解析ライブラリ
35F9.1
アプリ内イベントの経過時間測定
注意したいのは、理由コードは「それらしいもの」を選ぶのではなく、自分のアプリでの実際の使い方 に一致するものを選ぶという原則です。たとえば DiskSpace を AdMob のような広告 SDK が引いているだけで、自分のコードは空き容量を見ていないなら、E174.1(書き込み前のチェック)が実態に合うかを確認します。合わない場合は、その用途に対応する別の理由コードを Apple のドキュメントで確認してください。
ステップ3: アプリ側マニフェストに申告を集約する
ライブラリが自前の PrivacyInfo.xcprivacy を同梱していても、アプリ側のマニフェストが申告の正本 になります。審査は最終バイナリに含まれる申告の集合を見るため、自分のコードや「マニフェストを持たない古いライブラリ」が引くカテゴリは、アプリ側で明示的に申告しておくのが安全です。ios/PrivacyInfo.xcprivacy を次のように集約します。
<? xml version = "1.0" encoding = "UTF-8" ?>
< plist version = "1.0" >
< dict >
< key >NSPrivacyTracking</ key >
< false />
< key >NSPrivacyCollectedDataTypes</ key >
< array />
< key >NSPrivacyAccessedAPITypes</ key >
< array >
< dict >
< key >NSPrivacyAccessedAPIType</ key >
< string >NSPrivacyAccessedAPICategoryUserDefaults</ string >
< key >NSPrivacyAccessedAPITypeReasons</ key >
< array >< string >CA92.1</ string ></ array >
</ dict >
< dict >
< key >NSPrivacyAccessedAPIType</ key >
< string >NSPrivacyAccessedAPICategoryFileTimestamp</ string >
< key >NSPrivacyAccessedAPITypeReasons</ key >
< array >< string >C617.1</ string ></ array >
</ dict >
< dict >
< key >NSPrivacyAccessedAPIType</ key >
< string >NSPrivacyAccessedAPICategoryDiskSpace</ string >
< key >NSPrivacyAccessedAPITypeReasons</ key >
< array >< string >E174.1</ string ></ array >
</ dict >
< dict >
< key >NSPrivacyAccessedAPIType</ key >
< string >NSPrivacyAccessedAPICategorySystemBootTime</ string >
< key >NSPrivacyAccessedAPITypeReasons</ key >
< array >< string >35F9.1</ string ></ array >
</ dict >
</ array >
</ dict >
</ plist >
Rork Max は内部的に Expo ベースのプロジェクトを書き出すため、このファイルを毎回手で ios/ に置くより、Expo の config plugin として宣言 しておくと、prebuild のたびに自動で正しい位置へ生成されます。app.config.js に以下を加えると、ネイティブディレクトリを作り直しても申告が消えません。
// app.config.js
module . exports = {
expo: {
ios: {
privacyManifests: {
NSPrivacyAccessedAPITypes: [
{
NSPrivacyAccessedAPIType: "NSPrivacyAccessedAPICategoryUserDefaults" ,
NSPrivacyAccessedAPITypeReasons: [ "CA92.1" ],
},
{
NSPrivacyAccessedAPIType: "NSPrivacyAccessedAPICategoryFileTimestamp" ,
NSPrivacyAccessedAPITypeReasons: [ "C617.1" ],
},
{
NSPrivacyAccessedAPIType: "NSPrivacyAccessedAPICategoryDiskSpace" ,
NSPrivacyAccessedAPITypeReasons: [ "E174.1" ],
},
],
},
},
},
};
設定として持たせる利点は、ios/ を .gitignore していても申告がソース管理下に残ることです。手書きの PrivacyInfo.xcprivacy は prebuild で上書き・消失しがちなので、宣言を構成ファイル側に寄せておくのが堅実です。
ステップ4: アップロードする前に自分で弾く
ここが本稿のいちばん効くところです。ITMS-91053 が痛いのは、検出されるのがアップロードして半日後 だからです。本番にアップロードする前に手元で対処・回避できれば、往復のロスは消えます。やり方は二つあり、両方を仕込んでおくと安心です。
一つ目は、ビルドした .ipa/.xcarchive に含まれる集約済みマニフェストを自分で確認する方法です。次のスクリプトは、アーカイブ内の対象カテゴリと、アプリ側マニフェストで申告済みのカテゴリを突き合わせ、未申告のカテゴリがあれば失敗で返します 。
#!/usr/bin/env bash
# preflight-privacy.sh ARCHIVE_OR_IPA APP_MANIFEST
set -euo pipefail
TARGET = " $1 " ; MANIFEST = " $2 "
# バイナリ側で参照される対象カテゴリ
used = $( grep -rhoE "NSPrivacyAccessedAPICategory[A-Za-z]+" " $TARGET " 2> /dev/null | sort -u )
# アプリ側マニフェストで申告済みのカテゴリ
declared = $( grep -oE "NSPrivacyAccessedAPICategory[A-Za-z]+" " $MANIFEST " | sort -u )
missing = $( comm -23 <( echo " $used ") <( echo " $declared ") )
if [ -n " $missing " ]; then
echo "❌ 未申告のカテゴリがあります:" ; echo " $missing "
exit 1
fi
echo "✅ 申告漏れなし"
二つ目は、Apple 公式の検証をアップロード前に通すことです。Xcode Organizer の「Validate App」か、CLI なら xcrun altool --validate-app で、ITMS-91053 を含む配布前チェックを実行できます。EAS で eas submit する前にこの検証ステップを挟むだけで、リジェクトの大半は手元で止まります。
# 配布前にローカルで検証(資格情報はプレースホルダー)
xcrun altool --validate-app \
-f ./build/YourApp.ipa \
-t ios \
--apiKey "YOUR_ASC_KEY_ID" \
--apiIssuer "YOUR_ASC_ISSUER_ID"
設定を変えたのにリジェクトが繰り返される場合は、ビルドキャッシュが古い申告を抱えている可能性があります。eas build --platform ios --clear-cache で作り直し、検証も新しい成果物に対して行ってください。
ステップ5: 「毎リリース証明する」を仕組みに落とす
最後に、この三点を一度きりの作業ではなく、リリースのたびに自動で回る形にします。私が個人開発のアプリで実際に採っているのは、リリース用ビルドプロファイルにプリフライト検証を紐付けてしまう方法です。EAS のビルド完了後フックや、CI のリリースジョブに preflight-privacy.sh を組み込み、未申告カテゴリが一つでもあればリリースを止めます。
{
"build" : {
"production" : {
"ios" : { "buildConfiguration" : "Release" },
"env" : { "PRIVACY_PREFLIGHT" : "1" }
}
}
}
加えて、app.config.js の privacyManifests をコードレビューの対象に含め、新しいライブラリを追加する PR では「ステップ1の棚卸しスクリプトを回したか」をチェック項目にしておきます。こうしておくと、依存を足した人が申告のずれにその場で気づけます。npx expo-doctor をリリース前に回す習慣も、ライブラリ側マニフェストの欠落を早めに拾えるので有効です。
ITMS-91053 は、仕組みにしてしまえば「たまに来る面倒」から「アップロード前に必ず緑になる確認」へと性質が変わります。Apple は Required Reason API の対象を今後も広げていく方針を示しているため、申告を毎リリース再生成・再検証する運用に寄せておくほど、将来の追加要件にも自動で追従しやすくなります。
まず今日できるのは一つです。手元のプロジェクトで scan-required-reason.sh を回し、出てきたカテゴリが app.config.js の privacyManifests にすべて載っているかを照合してみてください。差分が見つかれば、それが次のリジェクトの芽です。今のうちに潰しておきましょう。お読みいただきありがとうございました。