Initialize CloudX and Google Mobile Ads before creating the controller. Keep your existing pacing, retry, and placement timing rules around this controller.
FirstLookInterstitialController.swift
import CloudXCore
import GoogleMobileAds
import UIKit
@MainActor
final class FirstLookInterstitialController: NSObject {
enum Source {
case cloudX
case adMob
}
protocol Listener: AnyObject {
func firstLookInterstitialController(_ controller: FirstLookInterstitialController, didLoad source: Source)
func firstLookInterstitialController(_ controller: FirstLookInterstitialController, didFailToLoad source: Source, message: String?)
func firstLookInterstitialController(_ controller: FirstLookInterstitialController, didShow source: Source)
func firstLookInterstitialController(_ controller: FirstLookInterstitialController, didFailToShow source: Source, message: String?)
func firstLookInterstitialController(_ controller: FirstLookInterstitialController, didClose source: Source)
func firstLookInterstitialController(_ controller: FirstLookInterstitialController, didClick source: Source)
}
weak var listener: Listener?
private weak var viewController: UIViewController?
private let cloudXInterstitial: CLXInterstitial
private let adMobAdUnitId: String
private var adMobInterstitial: InterstitialAd?
private var isLoadingCloudX = false
private var isLoadingAdMob = false
init?(
viewController: UIViewController,
cloudXAdUnitId: String,
adMobAdUnitId: String
) {
guard let cloudXInterstitial = CloudXCore.shared.createInterstitial(adUnitId: cloudXAdUnitId) else {
return nil
}
self.viewController = viewController
self.cloudXInterstitial = cloudXInterstitial
self.adMobAdUnitId = adMobAdUnitId
super.init()
self.cloudXInterstitial.delegate = self
}
func load() {
guard !isLoadingCloudX,
!isLoadingAdMob,
!cloudXInterstitial.isReady,
adMobInterstitial == nil else {
return
}
isLoadingCloudX = true
cloudXInterstitial.load()
}
func show() -> Bool {
guard let viewController else {
return false
}
if cloudXInterstitial.isReady {
cloudXInterstitial.show(from: viewController)
return true
}
return showAdMobFallback(from: viewController)
}
func destroy() {
cloudXInterstitial.destroy()
adMobInterstitial = nil
listener = nil
}
private func loadAdMobFallback() {
guard !isLoadingAdMob, adMobInterstitial == nil else {
return
}
isLoadingAdMob = true
InterstitialAd.load(with: adMobAdUnitId, request: Request()) { [weak self] ad, error in
guard let self else { return }
self.isLoadingAdMob = false
if let error {
self.adMobInterstitial = nil
self.listener?.firstLookInterstitialController(self, didFailToLoad: .adMob, message: error.localizedDescription)
return
}
guard let ad else {
self.adMobInterstitial = nil
self.listener?.firstLookInterstitialController(self, didFailToLoad: .adMob, message: nil)
return
}
ad.fullScreenContentDelegate = self
self.adMobInterstitial = ad
self.listener?.firstLookInterstitialController(self, didLoad: .adMob)
}
}
private func showAdMobFallback(from viewController: UIViewController) -> Bool {
guard let adMobInterstitial else {
return false
}
adMobInterstitial.present(from: viewController)
return true
}
}
extension FirstLookInterstitialController: CLXInterstitialDelegate {
func didLoad(_ ad: CLXAd) {
isLoadingCloudX = false
listener?.firstLookInterstitialController(self, didLoad: .cloudX)
}
func didFailToLoadAd(_ adUnitId: String, error: CLXError) {
isLoadingCloudX = false
loadAdMobFallback()
}
func didDisplay(_ ad: CLXAd) {
listener?.firstLookInterstitialController(self, didShow: .cloudX)
}
func didFailToDisplay(_ ad: CLXAd, error: CLXError) {
if let viewController, showAdMobFallback(from: viewController) {
return
}
listener?.firstLookInterstitialController(self, didFailToShow: .cloudX, message: error.localizedDescription)
}
func didHide(_ ad: CLXAd) {
listener?.firstLookInterstitialController(self, didClose: .cloudX)
}
func didClick(_ ad: CLXAd) {
listener?.firstLookInterstitialController(self, didClick: .cloudX)
}
}
extension FirstLookInterstitialController: FullScreenContentDelegate {
func adWillPresentFullScreenContent(_ ad: FullScreenPresentingAd) {
adMobInterstitial = nil
listener?.firstLookInterstitialController(self, didShow: .adMob)
}
func adDidDismissFullScreenContent(_ ad: FullScreenPresentingAd) {
adMobInterstitial = nil
listener?.firstLookInterstitialController(self, didClose: .adMob)
}
func ad(_ ad: FullScreenPresentingAd, didFailToPresentFullScreenContentWithError error: Error) {
adMobInterstitial = nil
listener?.firstLookInterstitialController(self, didFailToShow: .adMob, message: error.localizedDescription)
}
func adDidRecordClick(_ ad: FullScreenPresentingAd) {
listener?.firstLookInterstitialController(self, didClick: .adMob)
}
}
show() at the placement moment. If it returns false, neither CloudX nor the fallback had a ready ad, so continue the app flow without showing an ad.
if firstLookInterstitial?.show() != true {
continueToNextScreen()
}
load() again when your app is ready to prepare the next placement opportunity.
This sample keeps the fallback lazy: AdMob loads only after CloudX fails to load. Do not parallel-load both sources unless CloudX recommends a different rollout strategy for your app.