2014年から個人で iOS アプリを作り続けて12年目に入り、累計5,000万ダウンロードを超えるあたりからは、Apple のポリシー変更に振り回される時間が機能開発と同じくらいの比重を占めるようになりました。中でも 2024 年に必須化された Apple Privacy Manifest(PrivacyInfo.xcprivacy)は、個人開発者にとって厄介な領域です。理由は単純で、自分のコードだけでなく、依存している SDK の側にも対応版が揃っていないと申請が通らないからです。
Beautiful HD Wallpapers のように長期運用しているアプリでは、AdMob・Firebase Analytics・RevenueCat といった SDK が混在しています。あるとき AdMob を一段上のバージョンに上げたら、ストア申請で「Privacy Manifest に必要な NSPrivacyAccessedAPITypes が宣言されていない」と返ってきました。同じ更新を、別の癒し系アプリでは半年前に通しているのに、です。差分は一目では分からず、結局 1 日を Privacy Manifest の比較に溶かしました。本記事はその経験を踏まえ、Claude Code に追従作業をどこまで任せられるかを、運用中のワークフローと差分つきで書きます。
なぜ個人開発者ほど Privacy Manifest に振り回されるのか
Privacy Manifest は、Apple が 2024 年春以降のリリースで段階的に必須化した宣言ファイルです。アプリ本体だけでなく、Apple が指定する「commonly used APIs」を呼ぶ SDK にも、対応する宣言ファイルの同梱が求められました。個人開発者にこれが重く感じられる理由は、構造的に三つあります。
ひとつ目は、依存ライブラリの数に対して、各 SDK の対応速度が一律ではないこと。一般的な iOS アプリは AdMob、Firebase、Sign in with Apple、SwiftLint、Reachability 系ユーティリティ……と、すぐに 20 を超えます。個人開発では、これら全てを朝のメール 1 通で監視する仕組みを持っていない場合がほとんどです。
ふたつ目は、追加した瞬間は問題ない依存が、半年後の更新で突然 required reasons を要求してくること。Apple のリストはじわじわ拡張されており、UserDefaults・fileTimestamp・systemBootTime・diskSpace 等が後発で対象に加わりました。これらは古い個人開発アプリほど無意識に使っています。
みっつ目は、申請拒否時にメッセージが断片的なこと。リジェクト本文には「NSPrivacyAccessedAPITypes に対応する宣言が見当たりません」とだけ書かれるパターンが多く、どの SDK が原因で、どの API カテゴリに対する宣言が足りないのか、自分で照合する必要があります。
この 3 つを毎月の運用に組み込むには、AI に「監視と差分案の生成」を任せる仕組みが現実的です。
Privacy Manifest と Required Reasons API の関係を最短で整理する
ここから先の章のために、最低限の語彙だけ揃えておきます。
PrivacyInfo.xcprivacy には大きく 4 つのキーを宣言します。NSPrivacyTracking はトラッキングの有無、NSPrivacyTrackingDomains はトラッキングを行う通信先、NSPrivacyCollectedDataTypes はアプリが収集するデータの種類、そして NSPrivacyAccessedAPITypes が今回の主役で、Apple が指定する「commonly used APIs」を使う場合の理由を宣言する配列です。
Required Reasons API として現時点で対象となっているのは、File timestamp APIs・System boot time APIs・Disk space APIs・Active keyboard APIs・User defaults APIs などのカテゴリです。各カテゴリにはあらかじめ用意された理由コード(例: User Defaults APIs には CA92.1・1C8F.1・C56D.1・AC6B.1 といった ID)が割り当てられ、複数を配列で宣言できます。
実際のファイルはこういう形になります。
<!-- PrivacyInfo.xcprivacy の最小実装例 -->
<? xml version = "1.0" encoding = "UTF-8" ?>
<! DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
< plist version = "1.0" >
< dict >
< key >NSPrivacyTracking</ key >
< true />
< key >NSPrivacyTrackingDomains</ key >
< array >
< string >googleads.g.doubleclick.net</ string >
</ array >
< 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 >
</ array >
</ dict >
</ plist >
注意したいのは、NSPrivacyAccessedAPITypeReasons の値は理由コードであって、自由記述ではないという点です。User Defaults APIs の場合は CA92.1(同じアプリ内での読み書き)、AC6B.1(管理者からの設定取得)など、ユースケースごとに ID が決まっています。Claude Code に生成してもらうときは、後述するように Apple 公式の理由コード一覧をプロンプトに含めておくのがコツです。
依存ライブラリ追加・更新時に追従漏れが起きる典型パターン
実運用で見てきた追従漏れは、ほぼ次の 4 パターンに集約されます。
新規依存追加時に Pods 側の PrivacyInfo.xcprivacy を確認していない 。CocoaPods や SPM 経由で入った SDK の Privacy Manifest はリソースバンドルに同梱されるはずですが、まれに古いタグだけ用意されていて main ブランチでは未整備というケースがあります。
既存依存の更新時に required reasons が拡張されている 。AdMob のように Apple のリスト拡張に追従するタイプの SDK は、マイナーアップデートで NSPrivacyAccessedAPITypeReasons を一気に増やしてくるため、こちらの本体 xcprivacy も合わせて更新する必要があります。
自前コードで UserDefaults を新たに使い始めた 。アプリ内で設定保存のために UserDefaults.standard を追加したのに、NSPrivacyAccessedAPICategoryUserDefaults を宣言し忘れているパターン。
Swift Package のリソース宣言が抜けている 。Swift Package を SDK として配布する場合、Package.swift の resources に PrivacyInfo.xcprivacy を含めないと、ビルド成果物のフレームワーク内に同梱されません。
これらは目視レビューでは抜けやすく、申請の段になって初めて気づきます。逆に言えば、上記 4 つのチェックを CI でルーチン化できれば、リジェクトの大半は事前に潰せます。
Claude Code に Privacy Manifest のドラフトを書かせる
Claude Code に Privacy Manifest を扱わせるときは、プロジェクトのルートに docs/privacy-manifest-context.md のような参照ファイルを作っておき、Apple の Required Reasons カテゴリ一覧と、自前で使っている API・SDK を最初に共有するのが効果的です。私の場合は次のような構成にしています。
# docs/privacy-manifest-context.md の構成例
- カテゴリ別 Required Reasons の対応表(Apple 公式から最新を反映)
- 自前アプリで使っている UserDefaults キー一覧と用途
- 依存 SDK と、それぞれが既に同梱している xcprivacy のパス
- 過去にリジェクトされた具体例とその修正内容
その上で、Claude Code に次のようなプロンプトで初期ドラフトを依頼します。
# Claude Code 上での実行例(CLAUDE.md にも追記しておく)
claude << 'PROMPT'
docs/privacy-manifest-context.md と現行の PrivacyInfo.xcprivacy を読み、
次の条件で更新案を出してください。
1. 自前コードで使っている User Defaults・File timestamp・Disk space・
System boot time API のうち、xcprivacy に未宣言のカテゴリを列挙する。
2. それぞれのカテゴリに対し、推奨される理由コード(Apple 公式に従う)を
2〜3 案ずつ提示する。最も実装意図に近いものを 1 つ選び、理由を 1 行で書く。
3. 依存 SDK(Podfile.lock と Package.resolved を参照)が同梱している
xcprivacy で、本体側に追記が必要なものがあるかを列挙する。
出力は xcprivacy の差分パッチ形式(unified diff)にすること。
PROMPT
このプロンプトのポイントは、「最終的に差分パッチで返してもらう」ことを最初から指定している点です。差分形式にしておけば、後段で patch -p0 あるいは Claude Code の Edit ツール経由で安全に当てられます。自由記述で返ってくる更新案は、人間が手で書き写すと typo を埋め込みやすく、Privacy Manifest のように 1 文字でも壊れると申請に響くファイルとは相性が悪いと感じています。
もうひとつ重要なのは、Claude Code に「推奨される理由コード」を出させたあと、必ず Apple 公式のドキュメントと照合する手順を残すことです。後述するように、これは CI に組み込みます。Claude Code は要約と差分案の生成が得意ですが、Apple 側のカテゴリ・コードは年に数回更新されるため、ローカルに保存した参照ファイルが古ければ古い回答が返ります。
依存ライブラリの required reasons を本体に流し込む自動化
iOS アプリ本体の PrivacyInfo.xcprivacy は、依存 SDK の宣言と合算した形にする必要はありません。各 SDK が自分の Pod/Framework 内で宣言していれば、Xcode が成果物にまとめてくれます。けれど現実には、ある SDK が xcprivacy を同梱していない、または同梱しているがフレームワーク化のタイミングで漏れる、ということが起きます。これを検出するスクリプトを Claude Code に書かせると、運用が一気に楽になります。
たとえば、Pods ディレクトリと DerivedData の .framework を走査して、Privacy Manifest の有無を確認するスクリプトはこのようになります。
#!/usr/bin/env bash
# scripts/check-privacy-manifest-coverage.sh
set -eu
PROJECT_DIR = " ${1 :- $PWD } "
TARGET_PODS_DIR = "${ PROJECT_DIR }/Pods"
DERIVED_DATA_BASE = "${ PROJECT_DIR }/build"
echo "==> Checking PrivacyInfo.xcprivacy coverage under ${ TARGET_PODS_DIR }"
MISSING = ()
if [ -d "${ TARGET_PODS_DIR }" ]; then
while IFS = read -r pod_dir ; do
pod_name = $( basename "${ pod_dir }" )
if ! find "${ pod_dir }" -name "PrivacyInfo.xcprivacy" -type f \
-print -quit | grep -q . ; then
MISSING += ( "${ pod_name }" )
fi
done < <( find "${ TARGET_PODS_DIR }" -mindepth 1 -maxdepth 1 -type d)
fi
if [ "${ # MISSING [ @ ]}" -eq 0 ]; then
echo "OK: every Pod ships a PrivacyInfo.xcprivacy"
exit 0
fi
printf "WARN: %d pods without PrivacyInfo.xcprivacy\n" "${ # MISSING [ @ ]}"
printf ' - %s\n' "${ MISSING [ @ ]}"
exit 1
このスクリプトを Makefile か Fastlane Lane の前段に挟んでおき、出力が WARN を含む場合は CI を落とすようにします。落ちた場合は、MISSING に列挙された Pod のうち、Apple の Required Reasons API を使っていそうなものに対して、Claude Code に「同等の xcprivacy を生成して Pods/<name>/PrivacyInfo.xcprivacy に置く一時的な追加パッチ」を依頼します。あくまで暫定対応で、本筋は SDK 側の修正を待つことですが、申請を止めないための時間稼ぎとして有効です。
私の場合、AdMob・RevenueCat・Firebase 系を同時に扱う癒し系アプリで、SDK 側の追従が間に合わない時期にこの暫定対応で 3 週間しのいだ経験があります。3 週間は決して短くなく、これがなければ夏の繁忙期にアップデートを止める判断を迫られていました。
CI に Privacy Manifest の整合性チェックを組み込む
開発中はうっかり忘れがちですが、Privacy Manifest のチェックを CI に乗せておくと、毎回考えなくて良くなります。私の運用では、GitHub Actions に下記のような job を追加しています。
# .github/workflows/privacy-manifest.yml
name : Privacy Manifest
on :
pull_request :
paths :
- '**/*.swift'
- '**/*.m'
- 'Podfile.lock'
- 'Package.resolved'
- '**/PrivacyInfo.xcprivacy'
jobs :
check :
runs-on : macos-15
steps :
- uses : actions/checkout@v4
- name : Install CocoaPods
run : pod install --repo-update --silent
- name : Check Privacy Manifest coverage
run : bash scripts/check-privacy-manifest-coverage.sh "${GITHUB_WORKSPACE}"
- name : Detect new Required Reasons API usage
run : bash scripts/scan-required-reasons.sh "${GITHUB_WORKSPACE}"
- name : Compare manifest against Required Reasons inventory
run : |
python3 scripts/diff-privacy-manifest.py \
--manifest "App/PrivacyInfo.xcprivacy" \
--inventory "docs/required-reasons-inventory.json"
scripts/scan-required-reasons.sh は、UserDefaults・fileTimestamp を含む Apple Required Reasons の対象 API 呼び出しを grep -rn で抽出し、docs/required-reasons-inventory.json と突き合わせる薄いスクリプトです。これも Claude Code に生成させて構いません。重要なのは、人間が記憶しておかなくて良い形にすることです。
diff-privacy-manifest.py は、PrivacyInfo.xcprivacy に宣言されているカテゴリと、required-reasons-inventory.json に記録された「アプリ内で実際に使われているカテゴリ」を比較し、未宣言があれば exit code 1 を返す Python スクリプトです。これも 50 行程度で書けます。Claude Code に CLAUDE.md と一緒にプロンプトを渡せば、初回からほぼ動く実装が出てきます。
このような CI を 4 つの個人アプリに展開したところ、申請前のラリー(ストアレビューで戻されて修正する往復)が、それまでの月平均 2 回から 0〜1 回に減りました。0 回の月は、1 か月で半日くらいの工数を取り戻している計算です。
App Store Connect の拒否ログから差分パッチを生成する
それでも申請が拒否されることはあります。Apple Review からのメッセージは英語で、しかも該当箇所のサンプル行は付かないことが多いため、原因の切り分けに時間を取られます。私はこのフェーズに Claude Code を入れて、拒否ログをそのまま「修正パッチ」に変換させています。
ワークフローは次のようになっています。
# 1. App Store Connect から拒否メッセージ全文をクリップボードに保存
# 2. 該当アプリのリポジトリで Claude Code を起動
# 3. 下記プロンプトを実行
claude << 'PROMPT'
直前にクリップボードに保存した App Store Review の拒否メッセージと、
現在のリポジトリ内容(PrivacyInfo.xcprivacy・Podfile.lock・Package.resolved・
docs/required-reasons-inventory.json)を読みなさい。
次のステップを実行してください。
1. 拒否理由を 1 文に要約し、何が不足しているかを述べる。
2. 不足している宣言を補う xcprivacy のパッチを unified diff 形式で出力する。
3. 同じ拒否が再発しないための inventory.json の追記案を出力する。
4. 暫定対応で済むのか、SDK のアップデート待ちが必要かを判定する。
PR の作成は行わず、出力だけに留めてください。
PROMPT
このプロンプトのポイントは、「PR は作らない」と明示しているところです。Claude Code に PR まで任せてしまうと、再申請のリードタイムが詰まっている時にこちらの確認をスキップして上がってしまうことがあり、それで Privacy Manifest の宣言を必要以上に広げると、別の意味で不利になります(Apple は「使っていないものまで宣言する」ことも嫌います)。
このフローを 12 年もののアプリで何度も回した感触として、Claude Code は「拒否文の英語要約」と「xcprivacy の差分案」については極めて精度が高いです。一方、「暫定対応か SDK 更新待ちか」の判定は半分くらいは外します。SDK のリリースノートを Claude Code に渡せていないケースが多いため、ここは Brewfile や README で SDK の最新版情報を集約しておくと改善します。
個人開発で守りたい運用上の原則
最後に、12 年やってきて Privacy Manifest 周りで身につけた、人に勧められる範囲の原則を 3 つだけ書き残します。
ひとつ目は、Privacy Manifest を「コード」と同列で扱うこと。PrivacyInfo.xcprivacy の更新は、Swift のコード変更と同じ重みでレビューします。CODEOWNERS にも本人を入れ、依存追加 PR では必ず xcprivacy の差分が同伴することを CI で確認します。
ふたつ目は、推測でカテゴリを増やさないこと。Claude Code が「念のため」と提案してきた宣言は、必ず実装根拠と紐づけてから受け入れます。アプリで本当に使っていない API カテゴリを宣言してしまうと、Apple の意図(最小限の宣言を求める)から外れます。私は「required-reasons-inventory.json に対応行がないものはマージしない」というルールを CODEOWNERS とともに運用しています。
みっつ目は、リジェクト履歴を docs/rejection-history.md に残し、Claude Code がいつでも参照できるようにすること。これがあると、同じパターンのリジェクトに対して、初回より圧倒的に速い修正案が返ってきます。私の場合は、AdMob 関連の NSPrivacyAccessedAPITypeReasons 不足リジェクトが 3 回起きてからこの仕組みを入れ、以後同種のリジェクトは 0 件で来ています。
Privacy Manifest はそれ自体が機能ではなく、ユーザーの目に触れることもない地味な領域です。けれど個人開発の場合、ここでつまずくと「リリースができない」という致命的な詰まり方をします。Claude Code を使えば、この地味な領域に必要なエネルギーを大きく節約でき、その分を本来作りたかった機能やアートワークに回せます。次に Privacy Manifest を触る前に、まず docs/required-reasons-inventory.json と docs/rejection-history.md の 2 つだけ用意してみてください。ここから先のすべての自動化が、その 2 ファイルに集約されていきます。
お読みいただきありがとうございました。