iOS で広告収益が思ったより伸びないとき、原因の多くは配信面でも入札単価でもなく、ATT(App Tracking Transparency)の許可率にあります。私自身、個人開発で壁紙アプリを運用していて、同じ広告 SDK・同じ配置でも、ATT のオプトイン率が十数ポイント動くだけで eCPM がはっきり変わる場面を何度も見てきました。
Rork Max はネイティブ Swift を生成するので、React Native 版よりも ATT まわりを自分で細かく制御できます。ここを設計として組み立てられるかどうかが、リリース後の収益を左右します。
広告収益が伸び悩む本当のボトルネックは「入札」より「許可率」
広告の売上は、ざっくり「表示回数 × eCPM」で決まります。多くの個人開発者は表示回数(=リテンションや起動頻度)に注目しますが、eCPM 側の最大の変数が ATT の許可率です。
ユーザーが ATT で「許可」を選ぶと、アプリは IDFA(広告識別子)を取得でき、広告ネットワークはパーソナライズされた入札を行えます。「許可しない」を選ぶと IDFA はゼロ値になり、入札はコンテキスト情報だけの勝負になります。この差が、そのまま単価差として表れます。
ここで重要なのは、許可率はコンテンツやアプリの価値とは半分独立している、という点です。つまり「聞き方」を設計するだけで動かせる余地があります。私はここを、コードを書く前の「収益設計」として最初に決めるようにしています。
IDFA が eCPM に効く仕組みを、数字で押さえる
なぜ許可・不許可でこれほど差が出るのか。パーソナライズ広告は、ユーザーの興味関心に基づいてより高い単価の広告を出し分けられるためです。逆に IDFA が無いと、広告主は「誰に出しているか」が曖昧なため入札を絞ります。
私の運用実感に近い、おおまかな相場感を並べると次のようになります(アプリ・地域・配信面で変わるため、あくまで方向性としての目安です)。
ATT の状態 IDFA eCPM の傾向 収益への影響
許可(Authorized) 取得可 基準(100%) 最大化できる
不許可(Denied) ゼロ値 およそ 40〜60% 単価が大きく下がる
未決定(Not Determined) 取得不可 不許可と同等 プロンプト未提示の取りこぼし
「未決定」の行が見落とされがちです。プロンプトをそもそも出せていない、あるいは出すタイミングを誤って却下されると、許可を取れたはずのユーザーまで不許可相当になります。ここが最初に塞ぐべき穴です。
システムの ATT ダイアログは「一度きり」だから、出し方を設計する
ATT のシステムダイアログには、開発者泣かせの制約があります。表示できるのは実質一度きりで、ユーザーが一度「許可しない」を選ぶと、以降はアプリ内から再提示できません(設定アプリへ誘導するしかありません)。
だからこそ、いきなりシステムダイアログを出すのは得策ではありません。文脈が伝わらないまま出た許可要求は、反射的に却下されがちです。ここで効くのが「事前許可プライミング(pre-permission priming)」です。
手順としては次の3ステップで考えます。
まず自前の説明画面(プライミング画面)で、なぜトラッキング許可をお願いするのかを一言で伝える
ユーザーが「続ける」を押したときにだけ、システムの ATT ダイアログを出す
プライミング画面で「今はしない」を選んだ人には、システムダイアログを温存し、後日文脈が整ったときに再度プライミングだけ出す
この設計なら、システムダイアログという「一度きりの弾」を、許可してくれそうな人に絞って撃てます。
事前許可プライミング画面を Rork Max(SwiftUI)で実装する
Rork Max に「トラッキング許可の事前説明画面を作って」と依頼すると土台は出てきますが、ATT の状態遷移とタイミングは自分で握った方が確実です。私は生成されたコードを、次のような明示的な状態管理に整理し直しています。
プライミング画面の要件
広告 SDK 初期化の直前ではなく、ユーザーがアプリの価値を一度体験した後に出す
文面は「無料で使い続けられるように、関連性の高い広告を表示するため」といった正直な言い方にする
「続ける」「今はしない」の二択にし、後者でもアプリは普通に使える
実装コード
import SwiftUI
import AppTrackingTransparency
import AdSupport
// ATT の状態を SwiftUI 側で扱いやすくするためのラッパー
@MainActor
final class TrackingCoordinator : ObservableObject {
@Published var showPriming = false
// 起動時に一度だけ呼ぶ。未決定のときだけプライミングを予約する
func evaluateOnLaunch () {
let status = ATTrackingManager.trackingAuthorizationStatus
if status == .notDetermined {
// すぐには出さない。価値体験の後に showPriming を立てる
showPriming = false
}
}
// ユーザーがプライミング画面で「続ける」を押したときに呼ぶ
func requestSystemPrompt () async -> Bool {
let status = await ATTrackingManager. requestTrackingAuthorization ()
let authorized = (status == .authorized)
Analytics. log ( event : "att_result" ,
params : [ "authorized" : authorized])
return authorized
}
}
プライミング画面本体は、通常の SwiftUI シートで十分です。
struct TrackingPrimingView : View {
@EnvironmentObject var coordinator: TrackingCoordinator
var onFinish: ( Bool ) -> Void
var body: some View {
VStack ( spacing : 20 ) {
Image ( systemName : "hand.raised.circle" )
. font (. system ( size : 56 ))
Text ( "広告をあなたに合わせるために" )
. font (.headline)
Text ( "このアプリを無料で続けられるよう、関連性の高い広告を表示します。次の画面で「許可」を選ぶと、より役立つ広告が表示されます。" )
. font (.callout)
. multilineTextAlignment (.center)
. padding (.horizontal)
Button ( "続ける" ) {
Analytics. log ( event : "att_priming_continue" )
Task {
let ok = await coordinator. requestSystemPrompt ()
onFinish (ok)
}
}
. buttonStyle (.borderedProminent)
Button ( "今はしない" ) {
Analytics. log ( event : "att_priming_skip" )
onFinish ( false )
}
. font (.footnote)
}
. padding ()
}
}
ATT リクエストを呼ぶタイミング
requestTrackingAuthorization はアプリがフォアグラウンドでアクティブなときに呼ぶ必要があります。起動直後のスプラッシュ表示中に呼ぶと、ダイアログが出ずに .denied が返ることがあります。私は「最初のメイン画面が描画され、ユーザーが1〜2アクションした後」を基準にしています。
許可率を計測して、コピーとタイミングを改善する
プライミングは一度作って終わりではなく、計測して初めて価値が出ます。最低限、次の3つのイベントを取っておくと、どこで人が落ちているかが見えます。
プライミング画面の表示(att_priming_shown)
「続ける」押下(att_priming_continue)
システムダイアログの結果(att_result に authorized の真偽)
この3点から2つの率を計算します。プライミング画面での「続ける」通過率と、システムダイアログでの最終許可率です。
enum Analytics {
// 実サービスでは Firebase Analytics 等に置き換える
static func log ( event : String , params : [ String : Any ] = [ : ]) {
// 例: プライミング表示率と最終許可率を後で集計する
AnalyticsBridge.shared. record ( name : event, parameters : params)
}
}
計測してみると、コピーの一語や提示タイミングで通過率が動くのが分かります。私が壁紙アプリで試した範囲では、「価値を一度見せた後」に出すだけで、起動直後に出す構成より最終許可率が体感で1割以上変わりました。数値は必ず自分のアプリで取り直してください。
AdMob 初期化との順序を間違えない
ここが本番で最も取りこぼしやすい落とし穴です。AdMob(Google Mobile Ads SDK)を ATT の結果より先に初期化してしまうと、初回の広告リクエストが IDFA 無しで飛び、その分の単価を丸ごと落とします。
正しい順序は「同意管理(必要なら UMP)→ ATT の結果確定 → AdMob 初期化 → 最初の広告リクエスト」です。この直列部分は並列化してはいけません。私は起動処理を1つのブートストラップ関数にまとめ、ATT が確定するまで広告初期化を待たせるようにしています。
func bootstrapMonetization ( coordinator : TrackingCoordinator) async {
// 1) 必要なら同意(UMP)を先に確定
await ConsentManager.shared. gatherIfNeeded ()
// 2) ATT の結果を待つ(プライミング経由で許可/不許可が決まる)
_ = await coordinator. requestSystemPrompt ()
// 3) ここで初めて広告 SDK を初期化する
await AdNetwork.shared. start ()
// 4) 初期化完了後に最初の広告をロードする
AdNetwork.shared. preloadInterstitial ()
}
注意点として、requestTrackingAuthorization を二重に呼ばないことです。プライミング経由とブートストラップ経由で二回呼ぶ設計にすると、状態が競合してデバッグが難しくなります。呼び出し口は一箇所に集約してください。
もう一つ本番で効く注意点があります。EU 圏を配信対象に含める場合、UMP(同意管理プラットフォーム)の同意フォームと ATT は別物であり、両方を適切な順序で通す必要があります。私は「UMP を先に確定し、その後に ATT のプライミングを出す」順序に固定しています。逆にすると、地域によっては同意状態が未確定のまま広告初期化に進んでしまい、配信自体が止まることがあります。テストは必ず実機で、リセット済みの状態(設定アプリでトラッキングを未決定に戻す、あるいはシミュレータではなく新規インストール)から行ってください。シミュレータでは ATT の挙動が実機と一致しないことがあり、ここで時間を溶かしがちです。
よくある失敗と、私が採っている判断
起動直後にシステムダイアログを出す → 文脈が無く却下されやすい。私は必ずプライミングを挟みます
プライミングで断られたら二度と出さない → 後日、別の価値体験の後にプライミングだけ再提示する余地を残します(システムダイアログは温存されているため)
広告初期化を application(_:didFinishLaunchingWithOptions:) の先頭で走らせる → ATT より前になりがちなので、ブートストラップに移します
許可率を計測していない → 改善の打ち手が勘になります。最低限の3イベントだけでも入れておくことを推奨します
App Store 審査では、ATT を出すのに NSUserTrackingUsageDescription(Info.plist の説明文)が必須です。Rork Max 生成時に抜けていることがあるので、提出前に必ず確認してください。
次の一歩
まずは自分のアプリで att_priming_shown と att_result の2イベントだけを仕込み、今の最終許可率を測ってみてください。数字が見えると、コピーとタイミングのどちらを直すべきかが具体的に決まります。私自身、収益の改善はここを起点にすることが多く、広告単価を追う前に「許可率」という土台を整えるのが結局は近道だと感じています。