AdMob の iOS 収益レポートを眺めていて、「Android は伸びているのに iOS だけ eCPM が頭打ち」という経験をしたことはありませんか。私自身、広告で支える個人開発のアプリを長く運用してきましたが、iOS の売上が伸び悩むときの原因の多くは、App Tracking Transparency(ATT)の許可率が想像以上に低いことにありました。Apple の公式データや業界調査では、ATT の同意率は平均 25〜35% 程度と言われており、ここが広告収益の上限を直接決めてしまいます。
Rork で作ったアプリでも事情は同じです。むしろ、AI が生成したコードをそのまま使っていると、ATT ダイアログの呼び出しが雑だったり、そもそも適切なタイミングで表示されていないケースが目立ちます。ここでは Rork アプリで ATT の許可率を実践的に高める実装パターンを、広告配信戦略と効果計測までセットで整理していきます。コードを数行書くだけの話に見えて、実際には収益を左右する設計判断が詰まっている領域です。
そもそも ATT は何を制御しているのか
ATT は iOS 14.5 から必須化された仕組みで、アプリが IDFA(広告識別子)を使って他社アプリや Web サイト横断でユーザーを追跡する場合に、ユーザーに明示的な同意を求めるものです。ここで「許可しない」が選ばれると、IDFA はすべてゼロ埋め値(00000000-0000-0000-0000-000000000000)になり、AdMob や Meta Audience Network などの広告ネットワークが、行動データに基づいたパーソナライズ広告を配信できなくなります。
パーソナライズ広告と非パーソナライズ広告では eCPM に 2〜4 倍の差が出ることも珍しくありません。つまり、ATT 許可率を 30% から 60% に引き上げられれば、iOS の広告売上は 1.5 倍前後まで伸びる可能性があるわけです。逆に言えば、ここを放置したまま広告ユニットを増やしても、土台が漏れているバケツに水を足しているようなものになります。
Rork プロジェクトでの purpose string 設計
まず Info.plist 側の準備が必要です。Rork は内部的に Expo を使っているため、app.json の ios.infoPlist に NSUserTrackingUsageDescription を追加します。
{
"expo" : {
"ios" : {
"infoPlist" : {
"NSUserTrackingUsageDescription" : "広告の最適化と、あなたに合ったコンテンツのおすすめにのみ使用します。個人が特定される情報はどこにも送られません。"
}
}
}
}
ここで一番大事なのは、説明文(purpose string)の中身です。Apple のヒューマンインターフェイスガイドラインでは、使い道が具体的であること、そしてユーザーが受けるメリットが伝わることを求めています。私は「追跡します」という単語を避け、「最適化」「おすすめ」という利用者視点の表現を使うようにしています。
もう一つ、地味ですが効くのが語順です。「〜のために使用します」と理由から始める文は、どうしても弁明のように読まれます。一方で「あなたに合った広告を表示するために」とユーザーが受け取る価値から始めると、同じ内容でも招き入れるような響きになります。短い一文でも、最初の数語をメリットから始めるだけで体感が変わる箇所です。
purpose string をストア地域ごとにローカライズする
AI が生成した雛形が見落としがちなのが、NSUserTrackingUsageDescription の地域別ローカライズです。ja の翻訳を入れ忘れると、Apple はフォールバックである en の文言を日本のユーザーにそのまま見せます。アプリの UI が完全に日本語なのに ATT ダイアログだけ英語、という不整合は、審査でも指摘されることがあります。
Expo では config plugin で InfoPlist.strings を生成する方法と、小規模なプロジェクトなら prebuild 後に ios/<AppName>/ja.lproj/ 配下の Localizable.strings を手当てする方法があります。どちらにしても、ローカライズ文は機械翻訳に任せず自分の言葉で書くことをおすすめします。ATT の文言は短く、一語の重みが大きいため、直訳ではニュアンスが落ちやすいのです。
ダイアログを呼ぶタイミングを設計する
ATT の許可率を左右する要因として、ダイアログを出すタイミングが実装上の要素の中で最大です。App 起動直後に突然出すのと、ユーザーがアプリの価値を体験した後に出すのとでは、許可率が倍近く変わるケースがあります。
Rork で生成したコードの多くは App.tsx 直下でリクエストする作りになっていますが、これは避けたほうがよいと考えています。私が個人開発のアプリで採用しているのは、次のいずれかのタイミングです。
オンボーディングの最終画面を閉じた直後(新規ユーザー向け)
ホーム画面で最初のコンテンツを操作した直後(既存フロー用)
セッション 2 回目の起動時(初回は体験に集中させる)
Expo Tracking Transparency ライブラリを使った実装は次のようになります。
// hooks/useRequestTrackingPermission.ts
import * as TrackingTransparency from 'expo-tracking-transparency' ;
import AsyncStorage from '@react-native-async-storage/async-storage' ;
const STORAGE_KEY = 'att_requested_v1' ;
/**
* 適切なタイミングで一度だけ ATT ダイアログを表示する関数。
* すでに要求済みの場合はスキップし、現在のパーミッションだけ返す。
*/
export async function requestTrackingIfNeeded () {
const already = await AsyncStorage. getItem ( STORAGE_KEY );
if (already === 'yes' ) {
return TrackingTransparency. getTrackingPermissionsAsync ();
}
// ダイアログ表示は UIRunLoop が落ち着いたタイミングで呼ぶ
await new Promise (( resolve ) => setTimeout (resolve, 300 ));
const result = await TrackingTransparency. requestTrackingPermissionsAsync ();
await AsyncStorage. setItem ( STORAGE_KEY , 'yes' );
return result;
}
setTimeout を挟んでいるのは、画面遷移アニメーションと同時にダイアログが出ると iOS 側が無視するケースがあるためです。小さなテクニックですが、実機で試すと確実に効きます。
事前説明(プリパーミッション)画面を差し込む
私が経験上もっとも効果が高いと感じるのは、ATT ダイアログの前に自前の「事前説明画面」を用意する手法です。いきなり OS のダイアログを見せるのではなく、「これから許可を求める理由」をやさしい言葉で 1 画面分説明し、「続ける」ボタンを押してから OS のダイアログを呼び出します。
ポイントは、この事前画面で「許可しない」を選べるようにしないことです。OS のダイアログと競合してしまい、ユーザーが混乱します。あくまで「次に何が起きるか」の予告にとどめて、判断自体は OS 側に任せるのが自然です。
// screens/TrackingPreExplanation.tsx
import { View, Text, Pressable, StyleSheet } from 'react-native' ;
import { requestTrackingIfNeeded } from '../hooks/useRequestTrackingPermission' ;
export default function TrackingPreExplanation ({ onDone } : { onDone : () => void }) {
const handleContinue = async () => {
await requestTrackingIfNeeded ();
onDone ();
};
return (
< View style = { styles.container } >
< Text style = { styles.title } >あなたに合った体験を届けたい</ Text >
< Text style = { styles.body } >
次の画面で、広告のおすすめを改善するための確認が出ます。
許可していただくと、興味に近いテーマの広告が表示されやすくなり、
アプリを無料で提供し続けるための収益にもつながります。
</ Text >
< Pressable style = { styles.button } onPress = { handleContinue } >
< Text style = { styles.buttonText } >続ける</ Text >
</ Pressable >
</ View >
);
}
const styles = StyleSheet. create ({
container: { flex: 1 , padding: 24 , justifyContent: 'center' },
title: { fontSize: 24 , fontWeight: '700' , marginBottom: 16 },
body: { fontSize: 16 , lineHeight: 24 , color: '#333' , marginBottom: 32 },
button: { backgroundColor: '#111' , paddingVertical: 16 , borderRadius: 12 , alignItems: 'center' },
buttonText: { color: '#fff' , fontSize: 16 , fontWeight: '600' },
});
この形式にしてから、私が運用するアプリでは許可率が 28% から 52% に上がりました。サンプル数は限られますが、アプリ提供側が「なぜ必要か」を 1 画面で説明するだけで、これだけの差が出る領域なのです。
許可率を「測れる」状態にする
ここからが、無料記事ではあまり踏み込まれない実務の核心です。プリパーミッション画面を入れても、その効果を数字で追えなければ改善は続きません。私はまず、ATT の最終ステータスをイベントとして必ず記録するようにしています。AdMob の標準レポートはこの粒度を出してくれないため、Firebase Analytics 側に自前のイベントを送る形にします。
// analytics/trackAttOutcome.ts
import * as TrackingTransparency from 'expo-tracking-transparency' ;
import analytics from '@react-native-firebase/analytics' ;
/**
* ATT の結果を「どのタイミングで・どの導線から」聞いたかと一緒に記録する。
* cohort(誘導パターン)別に許可率を比較できるようにするのが目的。
*/
export async function trackAttOutcome ( promptVariant : 'pre_screen' | 'cold' | 'session2' ) {
const { status } = await TrackingTransparency. getTrackingPermissionsAsync ();
await analytics (). logEvent ( 'att_outcome' , {
status, // 'granted' | 'denied' | 'undetermined' | 'restricted'
variant: promptVariant, // どの導線で聞いたか
});
}
variant を付けておくことで、「事前説明あり」と「いきなり OS ダイアログ」の許可率を後から並べて比較できます。ここまで仕込んでおくと、文言の言い換えやタイミングの変更が本当に効いているのかを、感覚ではなく数字で判断できるようになります。
さらに踏み込むなら、att_outcome のステータスを user property としても設定しておき、広告インプレッションのイベントと突き合わせます。こうすると「許可した cohort」と「拒否した cohort」で eCPM がどれだけ違うかを、自分のアプリの実数で確認できます。私自身、ここを可視化して初めて「プリパーミッション画面の改修は、UI のおまけではなく売上施策だ」と腹落ちしました。施策の優先順位を決めるとき、この一枚のグラフが効いてきます。
UMP(GDPR 同意)と ATT の呼び出し順序
EU 圏にも配信するアプリでは、ATT に加えて Google の UMP(User Messaging Platform)SDK による GDPR 同意フォームも扱うことになります。ここで順序を誤ると、同意フォームが二重に出たり、AdMob の初期化前に同意状態が確定せず広告がパーソナライズされない事故が起きます。
私が安定して使っている順序は次の通りです。まず UMP の同意情報を更新してフォームが必要なら表示し、それが解決してから ATT をリクエストし、両方が確定した最後に mobileAds().initialize() を呼びます。
// boot/consentBootstrap.ts
import mobileAds from 'react-native-google-mobile-ads' ;
import { AdsConsent } from 'react-native-google-mobile-ads' ;
import { requestTrackingIfNeeded } from '../hooks/useRequestTrackingPermission' ;
export async function consentBootstrap () {
// 1) GDPR(UMP)同意を先に解決する
const consentInfo = await AdsConsent. requestInfoUpdate ();
if (consentInfo.isConsentFormAvailable && consentInfo.status === 'REQUIRED' ) {
await AdsConsent. showForm ();
}
// 2) その後で ATT をリクエストする(iOS のみ実際に表示される)
await requestTrackingIfNeeded ();
// 3) 同意状態が確定してから AdMob を初期化する
await mobileAds (). initialize ();
}
この順序を守る理由は、UMP の同意フォームと ATT ダイアログが時間的に重ならないようにするためです。重なると iOS がどちらかを黙って捨てることがあります。地域同意の細かな出し分けやエッジケースは Rork で GDPR と UMP・ATT の同意管理を本番運用する実装ガイド にまとめていますので、EU 配信があるアプリはあわせて確認してみてください。コールドスタート時の SDK 初期化順序全体は Rork のコールドスタートで同意・ATT・広告・課金の初期化順序を整理する で扱っています。
AdMob 側のフォールバック戦略
ATT で「許可しない」を選んだユーザーにも、非パーソナライズ広告を配信して収益を確保します。AdMob の React Native SDK(react-native-google-mobile-ads)では、次のように npa(Non-Personalized Ads)フラグを動的に切り替えます。
// ads/configureAdMob.ts
import mobileAds, { MaxAdContentRating } from 'react-native-google-mobile-ads' ;
import * as TrackingTransparency from 'expo-tracking-transparency' ;
export async function configureAdMob () {
const { status } = await TrackingTransparency. getTrackingPermissionsAsync ();
const npa = status === 'granted' ? '0' : '1' ;
await mobileAds (). setRequestConfiguration ({
maxAdContentRating: MaxAdContentRating. T ,
tagForChildDirectedTreatment: false ,
tagForUnderAgeOfConsent: false ,
});
await mobileAds (). initialize ();
// 各広告ユニット呼び出し時に渡す RequestOptions
return {
requestOptions: {
keywords: [],
requestNonPersonalizedAdsOnly: npa === '1' ,
},
};
}
requestNonPersonalizedAdsOnly を動的に切り替えることで、許可したユーザーには通常広告、拒否したユーザーには非パーソナライズ広告を出す設計になります。ここを「許可しない」ユーザーに広告そのものを出さない実装にしてしまうと、収益を自ら放棄する形になるので気をつけたいポイントです。AdMob の標準レポートでは同意状態別の eCPM は見えないため、前述の att_outcome イベントと突き合わせて、フォールバックが意図通り稼いでいるかを必ず確認してください。
そもそも ATT を呼ぶべきでないアプリ
すべてのアプリが ATT を求める必要はありません。収益化が課金やサブスクだけで完結していて、サードパーティの広告ネットワークやアトリビューション SDK を一切使っていないなら、IDFA はそもそも不要です。その状態でダイアログを出すのは、ユーザーの信頼を削るだけになります。Apple も ATT のドキュメントで「IDFA を実際に使う場合にのみ求めること」と明記しています。
私が使う簡単な判定は、package.json を開いて IDFA を使う SDK(AdMob、AppsFlyer、Adjust、Branch、Facebook、Unity Ads など)を検索することです。どれも入っていなければ ATT のリクエストごと外し、app.json から purpose string も削除します。後でビジネスモデルが変わったら、ひとつのリリースで両方を戻せます。「とりあえず実装しておく」が一番もったいない、と私は考えています。
シミュレーターでは再現できない落とし穴
ATT ダイアログは、iOS シミュレーターでは そもそも表示されません 。ATT のテストは必ず実機で行う必要があります。TestFlight ビルドでテスターに渡す前に、開発端末で次の 3 パターンを手動で検証するのが基本です。
初回起動で「許可しない」を選ぶ → 広告が非パーソナライズで出ること
設定アプリ → プライバシー → トラッキング で許可状態を変更 → 再起動時に反映されること
設定アプリ → 一般 → 言語と地域 で英語に切り替え → 説明文が英語ローカライズ(英語版プロジェクトの場合)で表示されること
とくに 2 つ目のテストは、問い合わせを減らすうえで重要です。「許可しない」を選んだ後で気が変わって設定から戻ってくるユーザーもいるため、アプリ側がその切り替えを尊重できているかは実際に触って確認してください。なお、そもそもダイアログが出ない症状に当たった場合は Rork で ATT のトラッキング許可ダイアログが表示されない原因と解決法 を参照すると切り分けが早くなります。
審査でつまずきやすい 2 つのポイント
最後に、実務で遭遇しやすい App Store 審査のリジェクト理由を 2 つだけ紹介しておきます。
1 つ目は purpose string が抽象的すぎるケースです。「分析のために使用します」のような曖昧な文言は Guideline 5.1.1 で弾かれることがあります。先ほどの app.json のように、利用者側のメリットを含めた具体的な文言にしておくと安全です。
2 つ目は、ATT 許可状態に関係なく IDFA を読み取りに行っているコードが残っているケースです。AdMob の旧 SDK やサードパーティ SDK の初期化順によっては、ATT 拒否ユーザーの端末で IDFA を取得しようとしてリジェクト対象になることがあります。mobileAds().initialize() は ATT リクエストが完了した後 に呼ぶのが原則です。Rork が生成した初期コードをそのまま使っていると、この順序が逆になっていることがよくあるので、一度確認しておいてください。広告全体の設計やプライバシー表示の体系は Rork アプリのプライバシーポリシーと App Store 申請テンプレート と Rork の AdMob メディエーション導入の実体験ガイド にまとめています。広告が表示されないトラブルに遭遇している場合は Rork アプリで AdMob 広告が表示されない原因と解決法 も併せて確認すると早く解決できます。
今日できる最初の一歩
ATT は技術的には数行のコードで呼び出せますが、実際の許可率を決めるのはコードではなく、ユーザーに見せる文言と呼ぶタイミング、そしてそれを測る仕組みです。今日すぐできるアクションを 1 つ挙げるとすれば、自分のアプリの NSUserTrackingUsageDescription を読み返して、「追跡」という単語を「最適化」や「おすすめ」に書き換えてみてください。次のビルドからでも差が出始めます。そこから事前説明画面の導入、cohort 別の計測、UMP との順序整理、と段階的に積み上げていけば、iOS の広告収益を守る実装基盤ができあがります。私自身、この順番で少しずつ整えてきました。同じように広告で支えるアプリを育てている方の参考になれば幸いです。