管理画面の前で手が止まった日
iOS の 4 アプリにメディエーションパートナーを足していたときのことです。AdMob の管理画面でグループの一覧を開くと、Bidding 型のパートナーと、これまで通りの並び順で動くウォーターフォールの行が、同じ画面に並んでいました。
どちらかを消すべきなのか。それとも、並べたままにしてよいのか。
その場で決めきれず、設定を一度閉じました。この記事は、そのとき考えたことと、最終的に下した判断の記録です。
2026年10月時点の内容です。画面名や項目名は変わりやすいので、実際の操作の前には公式ドキュメントでの確認をお願いいたします。
先に結論 — 置き換えず、足しました
私が選んだのは「ウォーターフォールを捨てず、ビッディングを上から足す」形です。
Liftoff・InMobi・Unity Ads は Bidding 型として、4 アプリ共通の iOS リワード用グループ(RWD-iOS)に加えました
AppLovin MAX は従来のウォーターフォールのまま 20 グループに加え、最適化のトグルを ON にして eCPM ベースの動的調整に任せています
切り替えの判断は、体感ではなくネットワーク別の収益ログで下します
「ビッディングへ移行する」という言い方が広まっていますが、実運用では完全移行より併用の期間のほうが長くなります。理由は次の章で整理します。
二つの方式を、運用する側の目線で比べる
仕組みの解説は公式にお任せして、ここでは「設定画面で何を触り、何が壊れやすいか」だけを並べます。
ビッディングは手間が少ない代わりに、うまく動いていないことに気づきにくい方式です。手元に「ネットワーク別の収益」が無い状態で切り替えると、良くなったのか悪くなったのかを後から言えなくなります。
併用を選んだ一番の理由はここでした。比べる物差しが先に要ります。 物差しが整う前に置き換えると、判断の根拠が残りません。
ATT と初期化の順序 — 入札が空振りする最初の罠
iOS では、トラッキング許諾のダイアログを出す前に広告 SDK を起動すると、入札に必要な識別子が揃わないままリクエストが走ることがあります。私のアプリでは ATTrackingManager.requestTrackingAuthorization を先に呼び、その完了後に MobileAds を初期化する順序にしています。
import AppTrackingTransparency
import GoogleMobileAds
// アプリがアクティブになったタイミングで呼ぶ(非アクティブ中はダイアログが出ません)
func startAdsAfterTrackingPrompt () {
ATTrackingManager. requestTrackingAuthorization { _ in
// 許諾の結果にかかわらず、ダイアログが閉じてから SDK を起動する
DispatchQueue.main. async {
MobileAds.shared. start ( completionHandler : nil )
}
}
}
なぜこう書くのか。許諾の結果は「許可」でも「拒否」でも構いません。大事なのは、ユーザーの選択が確定してから SDK が識別子を読みにいくことです。拒否された場合でも広告は配信されますが、入札の単価は下がります。それは仕様であり、不具合ではありません。
ここを誤っても、画面上は何も壊れません。広告は出ているので、気づかないまま収益だけが細くなります。
見落としやすい前段 — EU・英国向けの同意メッセージ
上のコードは ATT だけを扱っています。ところが、EU・英国のユーザーに広告を出すアプリでは、その前に Google の UMP(User Messaging Platform)で同意を取る工程が入ります。公式ドキュメントでは、起動のたびに同意情報を更新し、必要なフォームを表示してから、広告をリクエストする前に canRequestAds を確認する流れが案内されています。
import GoogleMobileAds
import UserMessagingPlatform
import UIKit
// 起動のたびに同意情報を更新し、必要ならフォームを出してから ATT → SDK 起動へ進む
func gatherConsentThenStartAds ( from vc: UIViewController) {
ConsentInformation.shared. requestConsentInfoUpdate ( with : RequestParameters ()) { error in
// 更新に失敗しても、前回の同意状態で広告を出せることがあるため、ここでは止めない
ConsentForm. loadAndPresentIfRequired ( from : vc) { _ in
guard ConsentInformation.shared.canRequestAds else { return }
DispatchQueue.main. async {
startAdsAfterTrackingPrompt () // 上で示した ATT → MobileAds 起動
}
}
}
}
canRequestAds は、同意情報の更新を一度も呼んでいないあいだは false を返します。つまり、更新の呼び忘れは「広告が出ない」という形で表に出ます。ATT の抜けより気づきやすい一方で、同意フォームの表示中にリクエストが走る順序のずれは、やはり収益に響きます。
ATT のダイアログを AdMob 側の IDFA メッセージで出す構成もあります。その場合は呼び出しの重複を避ける必要があり、順序の細部はお使いの SDK のバージョンで変わります。実装の直前に、公式の「プライバシーと同意」のページで確認をお願いいたします。私の環境では、ここで示した Swift のコードはこの記事のために実機で通して検証したものではなく、公式の API 名と流れに沿って書いた形です。
実装側で壊れやすい三か所
メディエーションを増やすと、広告を出す側の実装にも負荷がかかります。Android の更新で見直した箇所を三つ挙げます。
一つ目は、戻るボタンの広告ゲート。 入れ子の if で書くと、どの広告が優先されるのかがコードの形に埋もれます。並列で独立させ、優先順位をリストの順番として明示しました。
interface AdGate {
val name: String
fun isEligible (): Boolean // 表示条件(広告非表示状態の判定もここ)
fun show (onDone: () -> Unit)
}
class BackPressGates ( private val gates: List < AdGate >) {
// 優先順位は list の並び順。ここを見れば全体が分かる
fun onBackPressed (proceed: () -> Unit) {
val target = gates. firstOrNull { it. isEligible () }
if (target == null ) proceed () else target. show (proceed)
}
}
二つ目は、全画面系の同時表示。 課金の案内、レビュー依頼、リワード誘導のダイアログは、それぞれ単体では正しく動きます。ところが重なった瞬間に見え方が崩れます。中央モーダルを一か所で管理する層(ModalGate)を置き、同時に一つしか出さない約束にしました。
三つ目は、広告非表示状態の判定。 購入による非表示とリワードによる一時的な非表示を、isAdFree || isRewardAdFree の合成判定に必ず通すようにしています。判定の入口が複数あると、メディエーションを増やすたびに抜け道が増えます。この考え方は広告非表示と課金状態の Source of Truth を一つに寄せる記事 にまとめています。
切り替え前に、ネットワーク別の収益を残す
ここが、併用を続けるうえでの要です。広告が表示されるたびに、どのネットワークから、いくらで入ったかを Firebase に記録します。
import com.google.android.gms.ads.OnPaidEventListener
import com.google.android.gms.ads.rewarded.RewardedAd
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.logEvent
// 配信された広告ごとに、取得元のネットワーク名と収益を記録する
fun RewardedAd . trackPaidEvents (analytics: FirebaseAnalytics , placement: String ) {
onPaidEventListener = OnPaidEventListener { adValue ->
val source = responseInfo.loadedAdapterResponseInfo?.adSourceName ?: "unknown"
analytics. logEvent ( "ad_revenue_by_source" ) {
param ( "ad_platform" , "admob" )
param ( "ad_source" , source) // どのネットワークが勝ったか
param ( "ad_format" , "rewarded" )
param ( "ad_unit_name" , placement)
param (FirebaseAnalytics.Param.VALUE, adValue.valueMicros / 1_000_000.0 )
param (FirebaseAnalytics.Param.CURRENCY, adValue.currencyCode)
}
}
}
イベント名を ad_impression にしなかった理由があります。Firebase のドキュメントによると、AdMob と Firebase を連携しているアプリでは、ad_impression が自動で記録されます。自動記録の側には、標準の連携では value と currency が付きません。そこへ同じ名前で金額つきのイベントを手動で足すと、表示回数が二重に数えられる可能性があります。公式はこの二重計上について触れていないため、断言はできません。それでも、ネットワークを比べる目的なら、別名のイベントに分けておくほうが後で数字を疑わずに済みます。Google 広告側へ価値を渡したい場合は事情が変わるので、ad_impression の手動記録を選ぶときは、DebugView で件数を確かめてから進めてください。
ad_source を残しておけば、「ビッディング勢が勝った回数」「ウォーターフォール側が勝った回数」「それぞれの平均単価」を、同じ期間・同じ広告ユニットで並べられます。切り替えの判断は、この一枚の表で行います。
切り替えの前後で見るのは、次の五つです。
リリースは 5% → 25% → 50% → 100% の段階公開とし、Crash-free users が 99.7% 以上、ANR が 0.20% 未満を保っている間だけ次の段階へ進めています。収益が良く見えても、安定性が先です。
Antigravity に任せる範囲と、任せない範囲
エージェントに任せて良かったのは、次のような作業でした。
配信ログ(上のトラッキング)の実装と、既存コードへの差し込み
広告ゲートの並列化のような、構造の整理
テスト用広告ユニット ID の取り扱い(AGENTS.md に書いておくと、本番 ID の誤用を避けられます)
任せなかったのは、管理画面での収益に関わる設定です。パートナーの有効化、支払いプロファイル(Unity Ads・Liftoff・InMobi の各パートナー分)は、金額と権利に直結するため、自分の目で確認しながら進めました。
エージェントは実装を速くしてくれますが、「何を入札に出し、何を出さないか」という設計の責任までは預けられません。そこは私が引き受ける部分です。
関連する実装として、Flutter での広告統合や、Firebase のプロジェクト設定も合わせてご覧いただければ幸いです。
次の一歩
併用の期間を、まず4週間ほど取ってみてください。その間にやることは三つです。
広告表示のたびに ad_source を含めて収益を記録します
同意メッセージ → ATT のダイアログ → SDK 起動の順序になっているかを確認します
段階公開で、Crash-free users と ANR を見ながら進めます
物差しが揃ってから、残すものと減らすものを決めても遅くありません。置き換えを急がない判断も、運用では十分に価値のある選択だと考えております。振り返ると、最初に管理画面の前で手が止まったのは、迷いではなく、物差しが足りないという体のサインだったのかもしれません。