昨年の春、壁紙アプリの一本を更新して App Store に提出したところ、数時間後に「ITMS-91053: Missing API declaration」という通知が届きました。依存ライブラリのマニフェストは整えていたはずなのに、なぜ弾かれたのか。原因は自分のコードでした。設定値の保存に何気なく使っていた UserDefaults が、Apple の言う Required Reason API に該当していたのです。
Required Reason API の議論は、どうしても SDK 側の追従に寄りがちです。けれど申告が必要なのは、あなた自身が書いたコードも同じです。ここでは自コードの走査に的をしぼり、Claude Code を使って申告漏れを棚卸しし、PrivacyInfo.xcprivacy に理由コードを埋めるまでの流れを書きます。依存ライブラリ側の追従については別記事の依存ライブラリの追従漏れを防ぐ実装に譲ります。
なぜ「自コード」が見落とされるのか
Required Reason API は、名前が仰々しい割に、日常的に使う地味な API の集まりです。設定の保存に使う UserDefaults、ファイルの更新日時を読む modificationDate、空き容量を確認する volumeAvailableCapacityKey、端末の稼働時間を測る systemUptime。どれも「プライバシー」という言葉から連想する派手さがありません。だからこそ、宣言が要るという意識が働かないまま、コードの奥に沈んでいます。
まず、プロジェクト内の Swift ファイルから該当シンボルを検出します。Claude Code に「対象 API を4カテゴリに分類して検出し、ファイルと行番号を出す走査スクリプトを書いて」と頼み、出てきた原型に手を入れたのが次のものです。
#!/usr/bin/env python3"""scan_required_reason.py — 自コードの Required Reason API を検出する。"""import reimport sysfrom pathlib import Path# カテゴリ -> (検出シンボル, 個人開発でよく使う候補理由コード)CATEGORIES = { "UserDefaults": ( [r"\bUserDefaults\b", r"\bNSUserDefaults\b"], ["CA92.1"], ), "FileTimestamp": ( [r"\.modificationDate\b", r"\.creationDate\b", r"\bcontentModificationDateKey\b", r"\bfstat\b", r"\bstat\("], ["C617.1", "3B52.1", "0A2A.1"], ), "DiskSpace": ( [r"volumeAvailableCapacity\w*Key", r"\bsystemFreeSize\b", r"\bvolumeTotalCapacityKey\b"], ["85F4.1", "E174.1"], ), "SystemBootTime": ( [r"\.systemUptime\b", r"\bmach_absolute_time\b"], ["35F9.1"], ),}def scan(root: Path): hits = {} # category -> list[(file, line_no, snippet)] for swift in root.rglob("*.swift"): if "/Pods/" in str(swift) or "/.build/" in str(swift): continue for i, line in enumerate(swift.read_text(encoding="utf-8", errors="ignore").splitlines(), 1): for cat, (patterns, _codes) in CATEGORIES.items(): if any(re.search(p, line) for p in patterns): hits.setdefault(cat, []).append( (swift.name, i, line.strip()[:80])) return hitsif __name__ == "__main__": root = Path(sys.argv[1] if len(sys.argv) > 1 else ".") found = scan(root) if not found: print("該当なし。宣言が必要な自コード API は見つかりませんでした。") sys.exit(0) for cat, (_, codes) in CATEGORIES.items(): if cat in found: print(f"\n[{cat}] 候補理由コード: {', '.join(codes)}") for name, ln, snip in found[cat]: print(f" {name}:{ln} {snip}")
実際に運用してみて、いくつか注意点が見えてきました。ひとつは、stat のような一般的な C 関数名の誤検出です。文字列や無関係な文脈で一致してしまうため、検出行は必ず目視で確認します。もうひとつは、App Extension や Widget が別ターゲットとして独自の .xcprivacy を持つ点です。メインアプリだけ整えても、拡張側が漏れていれば同じ却下を受けます。走査の対象ルートに拡張のソースも含めておくと安全です。
理由コードの選択に迷ったときは、無理に一つへ絞らず、実態に合う複数を併記できるケースもあります。ただし「多めに書いておけば安心」という発想は避けたいところです。使っていない理由を並べるのは、宣言の趣旨から外れます。私は「そのコードが本当にその目的で API を使っているか」を一件ずつ確かめる方を選んでいます。