AdMob を単独で使うか、AppLovin MAX を加えたメディエーション構成に乗り換えるか。個人で iOS アプリを運用していると、この判断はどこかで必ず通る道だと感じています。私の場合は累計5,000万ダウンロード規模で運用してきた壁紙アプリのうち、まず4本に AppLovin MAX を段階導入することにしました。Antigravity Editor を主力エディタとして使い、Inline Edit と Agent Mode をどう使い分けるかも同時に検証する4週間でした。
この記事は「AppLovin MAX をどう統合するか」のチュートリアルではありません。むしろ「収益を落とさずに移行するために、どの順序で何を検証し、Antigravity のどの機能をどこで使ったか」という意思決定の記録です。SDK 連携の手順は公式ドキュメントを正としつつ、個人開発者が運用中のアプリで段階移行を進めるときの判断軸を共有します。
なぜ AppLovin MAX を加えるか、加えないかの判断
2014年から個人で iOS アプリを開発してきて、AdMob だけで運用していた時期が長くありました。AdMob は安定していて Google アカウントの管理に統一されている安心感が大きく、メディエーションを組まなくても運用が回ってきました。
それでも AppLovin MAX を検討し始めたのは、累計5,000万ダウンロードのアプリ群を3年ぶりにまとめて棚卸ししたとき、eCPM が国別に大きく違うことを Antigravity Agent に分析させた結果が決め手でした。北米とヨーロッパの一部地域では AdMob 単独の fill rate が落ちており、AppLovin のサーバーサイドビディングが効きそうな構造だと判断できたためです。一方で日本とアジア圏は AdMob で十分な数字が出ていて、変える理由はありませんでした。
この時点で「全アプリに一斉導入」ではなく「壁紙アプリ4本に段階導入し、収益指標が4週間でプラスを示したものから順次拡大」というプランに落ち着きました。両家の祖父が宮大工で「壊さずに継ぎ足す」という感覚を幼少から見て育っているせいか、運用中のアプリに対しては全替えではなく増設で進めたい感覚が強くあります。
4週間の進行スケジュールと検証ゲート
意思決定を共有資産にするために、4週間それぞれに目的とゲート条件を先に決めてから着手しました。Antigravity Editor のプロジェクトメモにこのスケジュールを置き、エージェントにも参照できる状態にしたのが結果的に重要でした。
週 目的 ゲート条件
第1週 SPM 経由で SDK を追加し、ビルドが通る・既存 AdMob 配信が壊れていないこと クラッシュフリーセッション 99.85% 維持・AdMob impression が前週 ±2% 以内
第2週 AppLovin の MAX Mediation Debugger でテスト広告が出ること、初期化順序を確定 テスト広告が4本のアプリすべてで再生・初期化の applicationDidFinishLaunching 内競合がない
第3週 本番配信を 10% から開始し、Remote Config でユーザーをサンプリング eCPM が AdMob 単独区と比較して有意差を取れる最低サンプル(impression 50,000以上)
第4週 国別に 30% / 50% / 100% の比率を地域で分けて拡大 ARPDAU が +3% 以上、もしくは ±0% で fill rate が改善していること
ゲート条件を満たさない場合は1週間そのままで、次週に進めないと決めていました。「来週には間に合わせる」と判断したくなる場面が必ず来るからです。
SDK 追加と初期化順序の確定(第1週)
第1週は SPM で AppLovin MAX SDK を追加する作業から始めました。CocoaPods から SPM への完全移行を済ませていたアプリと、まだ混在しているアプリがあり、混在側は CocoaPods で入れる選択もあり得ましたが、Antigravity Agent に「SPM 統一にした場合と CocoaPods 混在の場合で、12週間の運用負担はどう変わるか」を見積もらせて、結論として SPM 統一を採用しました。
初期化の順序は、AppLovin の公式ドキュメントでは早ければ早いほどよいとされていますが、AdMob を既に application(_:didFinishLaunchingWithOptions:) で初期化している場合に競合させない設計が必要です。私の場合は次のような共通コーディネータを書きました。これは Antigravity Editor の Inline Edit を中心に組み立てています。
import GoogleMobileAds
import AppLovinSDK
@MainActor
final class MediationCoordinator {
static let shared = MediationCoordinator ()
private ( set ) var isReady: Bool = false
private let queue = DispatchQueue ( label : "ad.mediation.init" )
private var didStart = false
func bootstrap ( completion : @escaping ( Bool ) -> Void ) {
queue. async { [ weak self ] in
guard let self , self .didStart == false else { return }
self .didStart = true
let group = DispatchGroup ()
group. enter ()
GADMobileAds. sharedInstance (). start { _ in
group. leave ()
}
group. enter ()
ALSdk. shared () ? .mediationProvider = "max"
ALSdk. shared () ? . initializeSdk { _ in
group. leave ()
}
group. notify ( queue : .main) {
self .isReady = true
completion ( true )
}
}
}
}
最初に書いた版では DispatchGroup を使わず if both done の状態管理を if-else で書いていて、片方が先に終わったときの分岐ミスでテストフライトのビルドがクラッシュしかけました。Antigravity Agent に「並列初期化で死ななくする一般的なパターンを提案してほしい」と聞き直して、DispatchGroup 版に書き直したのが第1週後半です。
エディタの Inline Edit は「この関数だけ書き換えたい」「この行だけ命名を整えたい」というときに圧倒的に速く、Agent Mode に投げるほどでもない小さな修正で1日30回近く使っていました。逆に Agent Mode は「並列初期化の一般的なパターンを示してくれ」「リファクタの選択肢を3つ並べてほしい」のように設計判断を含む依頼で使い、第1週は両方の使い分けを意識的に練習する週でもありました。
デバッガーで広告が出るまでの落とし穴(第2週)
第2週は MAX Mediation Debugger でテスト広告を出すところまで進めました。最初は「ad unit ID も SDK key も正しいのにテスト広告が出ない」状態が4本のアプリのうち2本で発生しました。
原因はシンプルで、Info.plist の SKAdNetworkItems に AppLovin の SKAdNetwork ID が一部しか入っていなかったためです。AdMob 単独運用時代の SKAdNetworkItems を引き継いでいたので、AppLovin が要求する最新リストとの差分が15件ほどありました。これを Antigravity Editor の Agent Mode に「公式ドキュメントの最新 SKAdNetwork ID リストと、現在の Info.plist の差分を取って、必要な追加分を XML で出してほしい」と依頼して、生成されたものを Info.plist にマージする形にしました。
< key >SKAdNetworkItems</ key >
< array >
< dict >
< key >SKAdNetworkIdentifier</ key >
< string >cstr6suwn9.skadnetwork</ string >
</ dict >
<!-- 既存の AdMob 由来の項目 -->
< dict >
< key >SKAdNetworkIdentifier</ key >
< string >ludvb6z3bs.skadnetwork</ string >
</ dict >
<!-- AppLovin が要求する追加分(実値は公式リストで都度更新) -->
</ array >
公式の SKAdNetwork ID リストは更新が入るので、ハードコードせずに「Antigravity Agent に最新版を確認してもらう運用」を選びました。アプリ更新のリリース直前にこの確認をルーチン化したことが、後の運用負担を大きく下げています。
第2週で意外と時間を使ったのが、ATT(App Tracking Transparency)のダイアログを出すタイミングの再調整です。AdMob のみの時代には起動直後にダイアログを出していたのですが、AppLovin のテスト広告ロード前に同意ステータスが確定していないと、最初の数回のセッションで広告が出ない事象がありました。これも Inline Edit ではなく Agent Mode に「ATT 同意取得を保証してから SDK 初期化を続けるラッパーを書いてほしい」と依頼して、初期化フロー全体を await で連結する設計に変更しています。
10% ロールアウトと Remote Config 設計(第3週)
第3週は本番配信を 10% から開始しました。ここで重要なのは「いきなり 100% に切り替えない」「Remote Config で切り戻し可能にする」「サンプル数が貯まるまで判断しない」の3点です。Firebase Remote Config を使って次のような分岐を入れました。
struct MediationConfig {
let enableAppLovin: Bool
let appLovinPercentage: Int
static func resolve () -> MediationConfig {
let remote = RemoteConfig. remoteConfig ()
let enabled = remote. configValue ( forKey : "mediation_enable_applovin" ). boolValue
let pct = Int (remote. configValue ( forKey : "mediation_applovin_pct" ).numberValue.intValue)
return MediationConfig ( enableAppLovin : enabled, appLovinPercentage : pct)
}
}
@MainActor
final class AdLoader {
func load ( unitId : String ) async throws -> Ad {
let config = MediationConfig. resolve ()
if config.enableAppLovin && shouldUseAppLovin ( percentage : config.appLovinPercentage) {
return try await MAX. load ( unitId : unitId)
}
return try await AdMob. load ( unitId : unitId)
}
private func shouldUseAppLovin ( percentage : Int ) -> Bool {
let bucket = UserBucket.current % 100
return bucket < percentage
}
}
UserBucket.current は端末固有のハッシュ値を100で割った剰余で、ユーザーごとに一貫した割り振りができるようにしてあります。これは累計5,000万ダウンロード規模で運用してきた中で何度か作り直してきた仕組みで、A/B テストや段階公開のたびに同じユーザーが同じ群に入ることを保証するためです。
サンプル数のゲートは「impression 50,000 以上で eCPM の信頼区間が ±5% に収まるか」で判定しました。実数で 50,000 という閾値は壁紙アプリの平均的な日次インプレッションから逆算したもので、アプリの規模に応じて調整が必要です。Antigravity Agent に「現在の DAU と平均 impression/user から、何日で 50,000 を超えるか試算してほしい」と聞いて、3〜5日で到達することを確認してから 10% ロールアウトを開始しました。
30% / 50% / 100% への拡大判断(第4週)
第4週は地域ごとに拡大率を分けました。北米とヨーロッパは 50% まで一気に上げ、東アジアは 30% に留め、東南アジアは 10% のまま据え置く判断です。AppLovin のメディエーションが効きやすい地域とそうでない地域を分けることで、リスクを地域ごとに切り分けられます。
Remote Config のキー設計を地域別に分けるか、単一キーで地域ロジックを Swift 側に持つかは迷いましたが、運用負荷を考えて単一キー+アプリ側ロジックの構成にしました。コードで地域判定する分には、Antigravity Editor の Inline Edit で Locale.current.region の判定を書き換えるのが圧倒的に速いためです。
struct RegionPolicy {
static func appLovinShare ( for region: String ? ) -> Int {
switch region {
case "US" , "CA" , "GB" , "DE" , "FR" : return 50
case "JP" , "KR" , "TW" , "HK" : return 30
case "TH" , "VN" , "ID" , "PH" : return 10
default: return 30
}
}
}
第4週終了時点で、北米とヨーロッパのアプリ2本では ARPDAU が +4.2% / +3.8% に改善しました。東アジアのアプリ1本は ±0% で fill rate のみ改善、東南アジアの1本は据え置きのため変化なしという結果になっています。「全アプリで一斉に上振れする魔法」ではないことが数値で確認できたのが、この4週間の最大の収穫でした。
Antigravity Editor の Inline Edit と Agent Mode の使い分け基準
4週間運用してみて、Inline Edit と Agent Mode の使い分けが自分の中で言語化できるようになりました。SDK 統合のような「公式ドキュメントが正で、それを実装に落とすだけ」の作業では Inline Edit が圧倒的に速く、設計判断を含む段になると Agent Mode の出番です。
具体的な基準として、次のように分けています。
Inline Edit を選ぶ場面は、関数の中の数行だけ書き換えたいとき、命名を統一したいとき、コメントを補足したいとき、エラーメッセージのテキストだけ調整したいとき、テストケースを1つ追加したいときなどです。Agent Mode と比べてレスポンスが速く、編集対象が見えているので結果の検証も即座にできます。
Agent Mode を選ぶ場面は、「並列初期化の一般的なパターンを示してほしい」「リファクタの選択肢を3つ並べてほしい」「公式ドキュメントの最新リストと差分を取ってほしい」「設計判断の根拠を整理してほしい」のような、設計や調査が含まれる依頼です。出力をそのまま採用するのではなく、複数の選択肢から選ぶための材料として使っています。
両方を1日に何十回も使う運用で4週間続けると、「これは Inline Edit でいい」「これは Agent Mode に投げよう」の判断が秒で出るようになりました。AI ツールの使い分けは結局のところ反復で身体化するしかないと感じます。
4週間の運用で得た意思決定の知見
この4週間で得た知見のうち、次に他のアプリへ展開するときに残しておきたいものを3つ挙げます。
第一に、段階公開の比率を決める前にゲート条件を書き出すこと。「impression 50,000」「ARPDAU +3%」のような具体数値を先に決めておくと、データが揃わないまま判断を進めようとする誘惑に流されません。第二に、初期化順序の競合は Inline Edit で直すのではなく、Agent Mode で一度パターンを整理してから書き直すこと。応急処置で if 文を増やすと、後で誰も触れない部分になります。第三に、Remote Config の Kill Switch を最初から入れること。何かが起きたときに即座に切り戻せる設計があるだけで、段階公開の判断に集中できます。
両家の祖父が宮大工で「壊さずに継ぎ足す」感覚を見て育ったと冒頭に書きましたが、収益が出ている運用中のアプリに対する変更は、まさにこの感覚が活きる場面でした。新しい SDK を「全替え」するのではなく、既存の AdMob を残したまま AppLovin MAX を「継ぎ足す」設計にしたことが、4週間で4本のアプリを大きな事故なく移行できた最大の理由だと感じています。
次のアクションは、AppLovin MAX の本格運用が始まったあと、12週間目に再度 ARPDAU と fill rate の数値を取り直して、運用を継続するアプリと、AdMob 単独に戻すアプリを判断することです。段階移行は導入で終わらず、12週間後の再評価まで含めて初めて完結します。