メインコンテンツまでスキップ

【iOS】App Open Ads(アプリ起動時広告)

App Open Ads(アプリ起動時広告)は、アプリの読み込み画面やバックグラウンドからの復帰画面を収益化するための全画面広告フォーマットです。ユーザーがアプリを起動したタイミングや、フォアグラウンドに切り替えたタイミングで表示されます。ユーザーはいつでも広告を閉じることができます。

実装は以下の3ステップで行います:

  1. 広告を事前にロードする
  2. アプリのライフサイクルイベントを監視し、適切なタイミングで広告を表示する
  3. デリゲートでコールバックを処理する

前提条件

  • VAMP SDK v5.3.5 以降(VAMP 全体の最小要件)
  • VAMPPangleAdapter 7.9.800 (Pangle SDK 7.9.0.8)以降
  • Xcode 26.1 以降(App Open Ads で利用する Pangle SDK の最小要件)
  • 対応アドネットワーク: 現時点では Pangle

各メソッドの仕様やオプションについては、iOSリファレンスをご参照ください。 テスト時の広告枠IDについてはテストを参照してください。

現時点の iOS App Open Ads は、VAMP SDK + VAMPPangleAdapter の組み合わせでご利用ください。VAMPPangleAdapter を Swift Package Manager(SPM)で導入すると、Pangle SDK も連動して解決されます。Ads-Global を個別に追加する必要はありません。

導入方法そのものは、以下のページを参照してください。

実装方針

安定した実装には、広告のロードに加えてアプリのライフサイクル制御が重要です。いつロードし、いつ表示し、いつ再ロードするか をアプリ側でも明確に管理してください。

実装時は、以下の考え方で組み込むことを推奨します。

  1. アプリ起動直後に広告をロードする
  2. バックグラウンド復帰時は、表示可能な広告がある場合のみ即表示する
  3. 復帰時に広告が未準備なら、その場では表示せず次回用のロードだけ行う
  4. 広告表示中は追加の load / show を行わない(App Open Ads 自身の表示中だけでなく、リワード広告など他フォーマットの全画面広告の表示中も show を行わない)
  5. 広告を閉じた直後、または表示失敗直後に次回用の広告を再ロードする

特に iOS では、didBecomeActive / sceneDidBecomeActive が広告表示中にも発火する場合があります。このタイミングで追加の load を実行すると、アプリ側の状態管理が壊れ、バックグラウンド復帰時に広告が表示されなくなる原因になります。

そのため、以下のような状態を持つ構成にすると実装しやすくなります。

  • isInitialLaunch
    • 初回起動時のみ、ロード完了後に即表示するためのフラグ
  • isLoadingAppOpenAd
    • 同じ placementID に対する二重ロードを防ぐためのフラグ
  • isShowingAppOpenAd
    • 広告表示中の didBecomeActive / sceneDidBecomeActive を無視するためのフラグ
  • isShowingOtherFullscreenAd

上記 4 つを使っておくと、初回起動・バックグラウンド復帰・広告クローズ後の再ロード・他フォーマットとの重なり防止を安全に制御しやすくなります。

広告の読み込み

次のコードスニペットでは、広告の読み込みをする場合の実装例です。

delegateにはVAMPAppOpenAdLoadDelegateを設定してください。

AdGeneration管理画面で発行された広告枠IDを*****に設定します。

import VAMP

class AppDelegate: UIResponder, UIApplicationDelegate, VAMPAppOpenAdLoadDelegate {
let placementId = "*****" // 広告枠IDを設定してください

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let request = VAMPRequest()
VAMPAppOpenAd.load(withPlacementID: placementId, request: request, delegate: self)
return true
}
}

広告の読み込みに成功した場合、VAMPAppOpenAdLoadDelegateappOpenAdDidReceiveWithPlacementID:デリゲートメソッドが呼ばれます。

※App Open Ads はアプリ起動の早い段階で読み込みを開始し、バックグラウンド復帰時にすぐ表示できるようキャッシュしておくことを推奨します。

load を呼ぶたびに必ず新規ネットワークリクエストが発生する前提で実装しないでください。App Open Ads は「表示可能な広告があるか」と「今ロード中か」をアプリ側でも管理し、不要な重複ロードを避けることを推奨します。

広告読み込み時のデリゲート

広告読み込み時の通知を受け取るためのVAMPAppOpenAdLoadDelegateのデリゲートメソッドについて解説します。

このデリゲートはVAMPAppOpenAdloadWithPlacementID:(広告読み込み時)で設定します。

広告表示準備完了

広告のロードが成功したタイミングで呼び出されます。

func appOpenAdDidReceive(withPlacementID placementID: String)

広告読み込み失敗

広告の読み込みに失敗した際に呼び出されます。

func appOpenAdDidFailToLoad(withPlacementID placementID: String, error: VAMPError)

詳細についてはエラーコード一覧を参照してください。

期限切れ

appOpenAdDidExpireWithPlacementID: は公開 API として定義されていますが、少なくとも現時点で対応している Pangle 連携では、このコールバックは通常利用されません。さらに、Pangle では有効期限超過を SDK 側で一律に appOpenAd:didFailToShowWithError: へ落とす前提にもなっていません。expire 通知や表示失敗を有効期限判定の唯一の契機とせず、広告を閉じた後や表示失敗後は必ず次回用の広告を再ロードしてください。

※有効期限についての詳細は「注意事項 > 広告の有効期限」を参照してください。 ※将来、対応アドネットワークの拡張にあわせて expire 通知の仕様を見直す可能性があります。

func appOpenAdDidExpire(withPlacementID placementID: String)

広告の表示

delegateにはVAMPAppOpenAdShowDelegateを設定してください。

if let appOpenAd = VAMPAppOpenAd.of(placementID: placementId) {
// 広告の表示
appOpenAd.show(from: self, delegate: self)
}

アプリのライフサイクルに統合する

バックグラウンド復帰時の表示

App Open Adsの主なユースケースは、アプリがバックグラウンドからフォアグラウンドに復帰したタイミングでの広告表示です。

UIKit ベースのアプリでは applicationDidBecomeActive(_:) を起点に制御できますが、Scene / SwiftUI ベースのアプリでは sceneDidBecomeActive(_:) 側で先に復帰を検知する場合があります。公開 sample でも、App Open Ads 制御は共通メソッドへ寄せ、UIApplicationDelegateUIWindowSceneDelegate の両方から同じ処理を呼ぶ構成を採用しています。

import UIKit
import VAMP

final class AppDelegate: UIResponder, UIApplicationDelegate {
private let placementId = "*****"
private var isInitialLaunch = true
private var isLoadingAppOpenAd = false
private var isShowingAppOpenAd = false
// リワード広告など他フォーマットの全画面広告の表示中は true にする
// (リワード側の rewardedAdDidOpen(_:) で true、
// rewardedAd(_:didCloseWithClickedFlag:) と
// rewardedAd(_:didFailToShowWithError:) の両方で false に戻す)
var isShowingOtherFullscreenAd = false

private var isSchedulingActiveRetry = false
private var activeRetryCount = 0
private let maxActiveRetryCount = 10
private var activeRetryTask: Task<Void, Never>?
private var activeRetryGeneration = 0

func application(_ _: UIApplication,
didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
loadAppOpenAd()
return true
}

func applicationDidBecomeActive(_ _: UIApplication) {
handleDidBecomeActive(resetRetryBudget: true)
}

func handleDidBecomeActive(resetRetryBudget: Bool = false) {
if resetRetryBudget {
activeRetryGeneration += 1
activeRetryTask?.cancel()
activeRetryTask = nil
isSchedulingActiveRetry = false
activeRetryCount = 0
}

guard UIApplication.shared.applicationState == .active else {
scheduleActiveHandlingRetryIfNeeded()
return
}

guard UIApplication.shared.topViewController() != nil else {
scheduleActiveHandlingRetryIfNeeded()
return
}

guard !isShowingAppOpenAd else {
return
}

activeRetryCount = 0
showAdIfAvailable()
}

private func loadAppOpenAd(with placementID: String? = nil) {
guard !isLoadingAppOpenAd else { return }

isLoadingAppOpenAd = true
let request = VAMPRequest()
VAMPAppOpenAd.load(withPlacementID: placementID ?? placementId,
request: request,
delegate: self)
}

private func showAdIfAvailable() {
guard !isShowingAppOpenAd else { return }

// リワード広告など他フォーマットの全画面広告の表示中は表示しない
guard !isShowingOtherFullscreenAd else { return }

guard let appOpenAd = VAMPAppOpenAd.of(placementID: placementId) else {
// 復帰時に広告が未準備なら、その場では表示せず次回用のロードだけ行う
loadAppOpenAd()
return
}

guard let viewController = UIApplication.shared.topViewController() else {
// 起動直後などで表示先 VC が未確定な場合は短時間だけ再評価する
scheduleActiveHandlingRetryIfNeeded()
return
}

isShowingAppOpenAd = true
appOpenAd.show(from: viewController, delegate: self)
}

private func scheduleActiveHandlingRetryIfNeeded() {
guard !isSchedulingActiveRetry else { return }
guard activeRetryCount < maxActiveRetryCount else { return }

isSchedulingActiveRetry = true
activeRetryCount += 1
activeRetryGeneration += 1
let generation = activeRetryGeneration

activeRetryTask = Task { @MainActor [weak self] in
guard let self else { return }

do {
try await Task.sleep(nanoseconds: 300_000_000)
} catch {
guard self.activeRetryGeneration == generation else { return }
self.isSchedulingActiveRetry = false
self.activeRetryTask = nil
return
}

guard self.activeRetryGeneration == generation else { return }

self.isSchedulingActiveRetry = false
self.activeRetryTask = nil
self.handleDidBecomeActive()
}
}
}

extension AppDelegate: VAMPAppOpenAdLoadDelegate {
func appOpenAdDidReceive(withPlacementID _: String) {
Task { @MainActor in
isLoadingAppOpenAd = false

// 初回起動時のみ、ロード完了後に即表示する
if isInitialLaunch {
showAdIfAvailable()
}
}
}

func appOpenAdDidFailToLoad(withPlacementID _: String, error _: VAMPError) {
Task { @MainActor in
isLoadingAppOpenAd = false
isInitialLaunch = false
}
}
}

extension AppDelegate: VAMPAppOpenAdShowDelegate {
func appOpenAdDidOpen(_ _: VAMPAppOpenAd) {
Task { @MainActor in
isInitialLaunch = false
}
}

func appOpenAd(_ appOpenAd: VAMPAppOpenAd, didCloseWithClickedFlag _: Bool) {
Task { @MainActor in
isShowingAppOpenAd = false
loadAppOpenAd(with: appOpenAd.placementID)
}
}

func appOpenAd(_ appOpenAd: VAMPAppOpenAd, didFailToShowWithError _: VAMPError) {
Task { @MainActor in
isShowingAppOpenAd = false
loadAppOpenAd(with: appOpenAd.placementID)
isInitialLaunch = false
}
}
}

final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func sceneDidBecomeActive(_ _: UIScene) {
(UIApplication.shared.delegate as? AppDelegate)?.handleDidBecomeActive(resetRetryBudget: true)
}
}

private extension UIApplication {
func topViewController(base: UIViewController? = nil) -> UIViewController? {
let windowScenes = connectedScenes.compactMap { $0 as? UIWindowScene }
let preferredWindow = (
windowScenes
.filter { $0.activationState == .foregroundActive }
.flatMap { $0.windows }
.first { $0.isKeyWindow }
) ?? (
windowScenes
.filter { $0.activationState == .foregroundInactive }
.flatMap { $0.windows }
.first { $0.isKeyWindow }
)

let baseViewController = base ?? preferredWindow?.rootViewController

if let navigationController = baseViewController as? UINavigationController,
let visibleViewController = navigationController.visibleViewController {
return topViewController(base: visibleViewController)
}

if let tabBarController = baseViewController as? UITabBarController,
let selectedViewController = tabBarController.selectedViewController {
return topViewController(base: selectedViewController)
}

if let presentedViewController = baseViewController?.presentedViewController {
return topViewController(base: presentedViewController)
}

return baseViewController
}
}

注意: バックグラウンド復帰の検知には UIApplication.willEnterForegroundNotification と、UIApplication.didBecomeActiveNotification / applicationDidBecomeActive(_:) 系の 2 つの選択肢があります。didBecomeActive 系はアプリがアクティブ状態に遷移した後に呼ばれるため、UI の準備が整った状態で広告を表示しやすく、より安全です。

※SceneDelegateを使用している場合は、UIWindowSceneDelegate.sceneDidBecomeActive(_:) からも同じ共通処理を呼ぶ構成にしてください。

Swift Concurrency(Swift 6 / Strict Concurrency Checking)をお使いの場合: AppDelegate などを @MainActor で宣言したクラスでデリゲートを実装する際は、必要なデリゲートメソッドに nonisolated を付け、UI 更新を含むプロパティの更新や SDK の呼び出しは Task { @MainActor in ... } 内で行うようにしてください。一方、このページの Swift コード例はクラス自体を @MainActor にしていない前提のため、nonisolated は不要です。Strict Concurrency Checking を有効にしたうえでクラスを @MainActor 化する場合のみ、nonisolated + Task { @MainActor in ... } のパターンを適用してください。

Sceneベースアプリでの実装上の注意

iOS 13 以降で SceneDelegate や SwiftUI App Lifecycle を利用しているアプリでは、applicationDidBecomeActive(_:) だけではバックグラウンド復帰時のイベントを安定して拾えない場合があります。

この場合は、アプリ全体で共通の App Open Ads 制御メソッドを 1 つ用意し、以下の両方から同じ処理を呼ぶ構成を推奨します。

  • UIApplicationDelegate.applicationDidBecomeActive(_:)
  • UIWindowSceneDelegate.sceneDidBecomeActive(_:)

ただし、Scene ベースアプリでは以下の 2 点に注意してください。

  1. 起動直後は sceneDidBecomeActive(_:) が先に発火し、まだ rootViewController や最前面の UIViewController が準備できていない場合がある
    このタイミングでは無理に show を行わず、必要なら短時間だけ再評価しつつ、初回 load が進んでいるならその結果を待ってください。 直上の Objective-C 例は self.window.rootViewController を使った簡略版です。SceneDelegate や SwiftUI App Lifecycle を使う構成では、そのまま固定の表示先として流用せず、表示直前に現在の最前面 UIViewController を解決してください。
  2. 広告表示中にも sceneDidBecomeActive(_:) が発火する場合がある
    このタイミングで追加の load を開始すると、アプリ側の状態が壊れ、以降の復帰時に広告が表示されなくなる原因になります。広告表示中は didBecomeActive 系で追加の load / show を行わない構成を推奨します。

初回起動時の表示方針

初回起動時は、ロード済み広告のキャッシュがまだ存在しません。そのため、以下のように考えてください。

  • didFinishLaunching またはそれに相当する初期化処理で load を開始する
  • 初回起動時のみ、appOpenAdDidReceiveWithPlacementID: を受けたタイミングで即 show する
  • 初回 load に失敗した場合は、その起動セッションでは表示できない可能性がある

初回起動時に「まだ広告がないから即座に再ロードする」「一定時間後に強制表示する」といった制御は推奨しません。App Open Ads は起動直後の自然な表示を想定したフォーマットのため、広告未準備のまま起動が進んだ場合は、そのセッションでは通常画面へ遷移してください。

コールドスタート時の表示

アプリの初回起動時(Cold Start)に広告を表示する場合、SDK初期化と広告ロードが完了するまで時間がかかる可能性があります。初回起動時はロード済み広告のキャッシュがないため、広告が表示されないことがあります。

初回起動時に広告を表示する必要がある場合は、ロード完了のデリゲート(appOpenAdDidReceiveWithPlacementID:)を受け取ったタイミングで表示してください。

広告表示時のデリゲート

VAMPAppOpenAdShowDelegateのデリゲートメソッドについて解説します。

このデリゲートはVAMPAppOpenAdshowFromViewController:(広告表示時)で設定します。

広告表示開始

広告の表示が開始されたタイミングで呼び出されます。

func appOpenAdDidOpen(_ appOpenAd: VAMPAppOpenAd)

広告を閉じる

広告を閉じたタイミングで呼び出されます。

func appOpenAd(_ appOpenAd: VAMPAppOpenAd, didCloseWithClickedFlag adClicked: Bool)

広告表示失敗

広告の表示に失敗した際に呼び出されます。

func appOpenAd(_ appOpenAd: VAMPAppOpenAd, didFailToShowWithError error: VAMPError)

詳細についてはエラーコード一覧を参照してください。

注意事項

広告表示中のロードについて

広告の表示中(appOpenAdDidOpen:からappOpenAd:didCloseWithClickedFlag:が呼ばれるまでの間)は、広告のロードを実行しないでください。表示中にロードを呼び出した場合、リクエストは無視されます。

広告を閉じた後(appOpenAd:didCloseWithClickedFlag:)または表示失敗後(appOpenAd:didFailToShowWithError:)に再ロードを行ってください。

復帰時の再表示方針

バックグラウンド復帰時は、以下のような挙動にすることを推奨します。

  • 表示可能な広告がある場合: 即表示する
  • 広告が未準備の場合: その場では表示せず、次回用に load だけ行う

このとき、「復帰した瞬間に広告が未準備だった場合、ロード完了後にそのまま後追いで自動表示する」挙動は推奨しません。ユーザーがすでに通常画面で操作を始めている可能性があるためです。

実装上は、showAdIfAvailable() の中で広告未準備時に loadAppOpenAd() を呼ぶだけに留め、後追い表示は行わない構成にしてください。

他フォーマットの広告表示中は表示しない

didBecomeActive / sceneDidBecomeActive は、リワード広告など他フォーマットの広告表示中にも発火する場合があります(例: リワード広告の再生中に広告をタップして Safari へ遷移し、アプリへ復帰した場合)。isShowingAppOpenAd は App Open Ads 自身の表示状態しか見ていないため、このケースでは復帰時の表示処理が素通りし、表示中のリワード広告の上に App Open Ads が重なって表示されます

SDK は他フォーマットの広告が表示中でも show の呼び出しをブロックしません。また、他フォーマットの表示状態を SDK に問い合わせる API はありません。重なりを防ぐには、アプリ側で「他フォーマットの全画面広告が表示中かどうか」を保持し、App Open Ads の表示判定に組み込んでください。

リワード広告と併用する場合の実装例です。VAMPRewardedAdShowDelegate のコールバックでフラグを更新します。

// リワード広告側のデリゲート実装(VAMPRewardedAdShowDelegate)
private var appDelegate: AppDelegate? {
UIApplication.shared.delegate as? AppDelegate
}

func rewardedAdDidOpen(_ rewardedAd: VAMPRewardedAd) {
appDelegate?.isShowingOtherFullscreenAd = true
}

func rewardedAd(_ rewardedAd: VAMPRewardedAd, didCloseWithClickedFlag adClicked: Bool) {
appDelegate?.isShowingOtherFullscreenAd = false
}

func rewardedAd(_ rewardedAd: VAMPRewardedAd, didFailToShowWithError error: VAMPError) {
// 表示失敗時に戻し忘れると、フラグが立ったままになり
// 以後 App Open Ads が表示されなくなります
appDelegate?.isShowingOtherFullscreenAd = false
}

フラグの更新は必ず上記の 3 箇所(rewardedAdDidOpen: で ON、rewardedAd:didCloseWithClickedFlag:rewardedAd:didFailToShowWithError:両方で OFF)をセットで実装してください。App Open Ads 側は「バックグラウンド復帰時の表示」のコード例のように、showAdIfAvailable の先頭でこのフラグを確認して表示をスキップします。

アプリ側で管理すべき状態

App Open Ads を安定して組み込むには、SDK の load / show を呼ぶだけでなく、アプリ側で以下の状態を管理してください。

  • isInitialLaunch
    • 初回起動時のみ、ロード完了後に即表示するために使用
  • isLoadingAppOpenAd
    • 二重ロードを防止するために使用
  • isShowingAppOpenAd
    • 広告表示中の active 通知を無視するために使用
  • isShowingOtherFullscreenAd

これらの状態を持たずに、didBecomeActive / sceneDidBecomeActive のたびに単純に load / show を呼ぶ実装は避けてください。

実装者が誤りやすいポイント

  • willEnterForeground だけで制御してしまう
    UI の準備前に広告表示を試み、show が空振りしやすくなります。
  • 広告表示中の active 通知で再度 load してしまう
    復帰時に広告が表示されなくなる原因になります。
  • リワード広告など他フォーマットの広告表示中の active 通知で show してしまう
    表示中の広告の上に App Open Ads が重なって表示されます。「他フォーマットの広告表示中は表示しない」のフラグ管理で抑止してください。
  • rootViewController がまだ存在しないタイミングで show してしまう
    起動直後や Scene 初期化中は、表示できずに終わる場合があります。
  • 起動直後の UI 準備前や onAppear で ATT など別の許諾ダイアログを要求してしまう App Open Ads の表示導線と競合し、広告表示や許諾ダイアログのどちらも空振りする場合があります。初回許諾が必要な場合は、広告表示導線と競合しない active 状態で要求してください。
  • 初回起動時とバックグラウンド復帰時を同じロジックで扱ってしまう
    初回起動時は didReceive 契機の即表示、復帰時は ready なら表示・未準備なら次回用ロード、という違いを分けて実装してください。
  • 広告を閉じたあとに再ロードしない
    次回復帰時に即表示できなくなります。

広告の有効期限

ロード済みの広告にはキャッシュ有効期限があります。有効期限を過ぎた広告を表示した場合、有効なインプレッションとしてカウントされず、収益に影響する可能性があります。

Pangle の有効期限ポリシーは 1 時間です。 1時間を超えた広告表示は有効インプレッションとしてカウントされません。

ただし、現行の iOS App Open Ads 実装では、Pangle は有効期限超過を SDK 側で一律に appOpenAd:didFailToShowWithError: へ落とす前提にはなっていません。Pangle 連携時は expire 通知や表示失敗だけに依存せず、広告を閉じた後や表示失敗後に必ず再ロードする構成を前提にしてください。

appOpenAdDidExpireWithPlacementID: は公開 API として定義されていますが、少なくとも現時点の Pangle 連携では通常通知されません。詳細は「広告読み込み時のデリゲート > 期限切れ」を参照してください。

表示頻度の制御

バックグラウンドからの復帰のたびに広告を表示すると、ユーザー体験が悪化する可能性があります。以下のような制御を検討してください。

  • 前回の広告表示からの最低インターバル(例: N分以上経過)
  • 1日あたりの表示回数上限