App Store 審査で意外と落ちるのが ATT 周りです
Rork でアプリを作って TestFlight まで動かし、いざ App Store に申請したら「App Tracking Transparency の説明が足りません」と返されて何日かロスする — これは私が3度ほど経験したパターンです。コードはちゃんと動くのに、Info.plist の文言と、プロンプトを出すタイミングと、ユーザーが拒否したときの分岐の3点が揃っていないと、Apple のレビュアーは納得してくれません。
ATT は iOS 14.5 から必須化されたプライバシー機構で、IDFA(広告識別子)など他社アプリ・サイトをまたいだトラッキング情報を取得する場合は、ユーザーから明示的な許可を取る必要があります。AppsFlyer / Adjust / Google Mobile Ads など、広告系・アトリビューション系の SDK を入れている時点で、ほぼ全員が対象になります。
ここでは Rork(React Native + Expo ベース)でアプリを開発している前提で、expo-tracking-transparency を組み込んで審査を一発で通すための手順をまとめます。コードと一緒に、申請文の書き方・タイミング設計・拒否時のフォールバックまで踏み込んで解説します。
ATT が必要になる典型的な3パターン
まず最初に、自分のアプリに ATT 実装が必要かを判断しておきましょう。次のいずれかに該当するなら必須です。
- 広告 SDK を使う: AdMob、Meta Audience Network、Unity Ads、AppLovin など、ほぼすべての広告ネットワーク
- アトリビューション SDK を使う: AppsFlyer、Adjust、Branch、Singular など、インストール元を追跡するもの
- アナリティクス SDK で IDFA を扱う: Firebase Analytics で広告 ID を有効化している場合、Mixpanel の一部設定など
逆に、自社サーバーに保存する独自の UUID だけで済んでいて、IDFA を一切触らないアプリは ATT プロンプトを出す必要がありません。とはいえ Apple は「触っていないと言いつつ間接的に触っているケース」を厳しく見るので、SDK のドキュメントで IDFA 利用の有無を必ず確認してください。
Step 1: Info.plist に説明文を追加する
Rork プロジェクトでは app.json(または app.config.js)に ios.infoPlist を追加し、NSUserTrackingUsageDescription キーを設定します。
{
"expo": {
"ios": {
"bundleIdentifier": "com.example.myapp",
"infoPlist": {
"NSUserTrackingUsageDescription": "あなたに合った広告を表示し、サービスを無料で提供し続けるために、利用状況の情報を使わせていただけませんか。許可しなくてもアプリのすべての機能はそのままお使いいただけます。"
}
}
}
}ここで重要なのが説明文の中身です。Apple のレビュアーは次の3点を見ています。
- 何のために使うかが具体的か: 「広告のため」「分析のため」だけでは弱く、ユーザーにとっての利益が伝わる必要があります
- 拒否しても利用に支障がないと明記しているか: 必須権限のような書き方は審査で弾かれます
- 誤解を招く表現がないか: 「機能改善のため」だけだと曖昧で、実態(広告 ID 利用)と齟齬があると判断されます
私が通った文面は、上記サンプルのように「広告」「無料提供」「拒否しても全機能使える」の3点を必ず入れる構成です。
Step 2: expo-tracking-transparency でプロンプトを呼ぶ
Rork はプロジェクトに Expo モジュールを追加できるので、expo-tracking-transparency を使うのがいちばん安全です。生成されたコードに次のように手を入れます。
// app/_layout.tsx などのエントリ付近
import {
getTrackingPermissionsAsync,
requestTrackingPermissionsAsync,
} from 'expo-tracking-transparency';
import { Platform } from 'react-native';
import { useEffect } from 'react';
export async function ensureTrackingPermission(): Promise<boolean> {
// iOS のみ実行(Android は対象外)
if (Platform.OS !== 'ios') return true;
// 14.5 未満は API 自体が存在しないので true 扱い
const current = await getTrackingPermissionsAsync();
if (current.status === 'granted') return true;
if (current.status === 'denied') return false;
// undetermined の場合だけプロンプトを出す
const result = await requestTrackingPermissionsAsync();
return result.status === 'granted';
}ここで気をつけたいのが、undetermined(未決定)のときだけ requestTrackingPermissionsAsync を呼ぶ点です。一度ユーザーが「許可しない」と答えた状態で再度呼んでもプロンプトは表示されず、純粋に denied が返るだけ。再表示するには「設定」アプリを開いてもらう必要があります。
Step 3: プロンプトを出すタイミングを設計する
Apple は「適切なタイミングでプロンプトを表示すること」を推奨しており、これを守らないと審査でリジェクトされます。私の経験上、起動直後にいきなり出すのは弱手で、次のような流れが通りやすいです。
- 起動直後はオンボーディング画面を見せる(プロンプトは出さない)
- オンボーディングの最後で「広告について」を1画面挟み、「このアプリは無料でお届けするため広告を表示します。次の画面で表示される確認に『App にトラッキングを許可』を選んでいただけると、より興味に合った広告をお見せできます」とユーザーに事前説明する
- その画面の「次へ」ボタンで
requestTrackingPermissionsAsync()を呼ぶ
この 事前説明(プリプロンプト) を入れると、許可率が体感で1.5〜2倍ほど変わります。Apple もこのパターンを公式サンプルで推奨しているので、審査の心象も良くなります。
Step 4: 拒否されたときの後続フロー
ATT で「許可しない」と回答したユーザーに対して、広告を一切出さない・サービスから締め出す、という対応は禁止です(ガイドライン 3.2.2)。やるべきは、IDFA を使わない広告配信に切り替えることです。
import mobileAds from 'react-native-google-mobile-ads';
export async function configureAds(): Promise<void> {
const trackingAllowed = await ensureTrackingPermission();
await mobileAds().setRequestConfiguration({
// 拒否時は非パーソナライズ広告のみ
tagForChildDirectedTreatment: false,
maxAdContentRating: 'T',
});
if (!trackingAllowed) {
// npa=1 は AdMob で非パーソナライズ広告を要求するフラグ
await mobileAds().initialize();
return;
}
await mobileAds().initialize();
}具体的な広告リクエストでは、AdMob の場合 requestNonPersonalizedAdsOnly: !trackingAllowed を渡します。AppsFlyer や Adjust 系のアトリビューション SDK は、disableIDFA のようなオプションが用意されているので、許可状況を渡して切り替えてください。
そして iOS は SKAdNetwork という、IDFA を使わずにインストール計測ができる仕組みを Apple 公式で用意しています。広告主 ID の 1:1 計測は無理ですが、キャンペーン単位の効果測定は可能です。本格的なマーケ予算を投じるアプリなら、SKAdNetwork の対応も合わせて進めるのが現実的な落としどころです。
Step 5: 私が審査で詰まった具体的なポイント
ここまでで実装と設計の話を書きましたが、レビューで実際に詰まった点を共有します。同じ落とし穴を避けるための参考にしてください。
- 説明文に「広告」と書いていなかった: 「サービス改善のため」とだけ書いて出したら、レビュアーから「広告 SDK が含まれているのに広告と明記されていない」と指摘されました。SDK の中身に応じた具体性が必要です
- オンボーディング前にプロンプトを出していた: 「ユーザーに何のためか分からない状態でプロンプトが出る UX は不適切」とリジェクト。事前説明画面を1枚挟んで通りました
- 設定画面に再許可導線がなかった: 一度拒否したユーザーが心変わりしたとき、設定アプリへ誘導する
Linking.openURL('app-settings:')を仕込んでいないと、内部レビューで使い勝手が悪い扱いになります
これらは Apple Developer のドキュメントには明示されていない、レビュアーの現場判断の部分です。ただ、リジェクトのフィードバックを読むと、上記3点は繰り返し出てくる定番の指摘でした。
まずは Info.plist の文面から見直してみてください
ATT 対応は「コードを書く前に文面を整える」作業のほうが審査結果を左右します。今すぐ自分のアプリの app.json を開いて、NSUserTrackingUsageDescription に「広告」「無料提供」「拒否しても全機能使える」の3点が含まれているか確認してみてください。それだけで、リジェクト確率はかなり下げられます。
実装の細かい部分でつまずいたら、関連するアトリビューション SDK 側の挙動も合わせて見直す必要があります。アトリビューションの全体設計は Rork で AppsFlyer / Adjust を使ったアトリビューション計測ガイド で、認証関連の権限 UX 全般は Rork アプリに生体認証(Face ID / Touch ID)を実装する完全ガイド でまとめています。申請手順全体を確認したい場合は Rork アプリの App Store 審査チェックリスト も合わせて読むと、提出前の漏れが減ります。
Expo に閉じない、ネイティブ側の知識が補えます。