View as Markdown

iOS

The Ezoic Ads SDK for iOS lets you display banner, rewarded, interstitial, native, outstream video, and instream video ads in native iOS apps. The SDK initializes Ezoic configuration, Prebid header bidding, Google Ad Manager, and consent handling from one integration point.

Requirements

  • iOS 15.0 or higher (SDK 1.13.0 raised the minimum from iOS 14.0)
  • Xcode 26.0 or higher (the SDK is built with Xcode 26.6; Xcode 27 is supported)
  • Swift 5.9 or higher
  • Google Mobile Ads application ID

Installation

The current SDK version is 1.13.2. The SDK is distributed as a pre-built EzoicAdsSDK.xcframework attached to each GitHub release. Swift Package Manager and CocoaPods both resolve the Prebid Mobile and Google Mobile Ads SDKs as transitive dependencies — you do not need to add them yourself.

Swift Package Manager

In Xcode, go to File > Add Package Dependencies and enter:

https://github.com/ezoic/ezoic-swift-sdk-dist.git

Select Up to Next Major Version starting at 1.13.2, or select Exact Version if you want every SDK update to be intentional.

Or add the package to Package.swift:

dependencies: [
    .package(url: "https://github.com/ezoic/ezoic-swift-sdk-dist.git", from: "1.13.2")
]

CocoaPods

Add the pod to your Podfile, then run pod install:

pod 'EzoicAdsSDK', '~> 1.13'

Initialize the SDK

Initialize the SDK in your AppDelegate or app startup flow before loading ads.

import EzoicAdsSDK

let config = EzoicConfiguration(
    domain: "example.com",
    debugEnabled: false,
    testMode: false
)

EzoicAds.shared.initialize(with: config) { result in
    switch result {
    case .success:
        print("Ezoic SDK initialized")
    case .failure(let error):
        print("Ezoic initialization failed: \(error)")
    }
}

domain must match the domain configured for your site in Ezoic. Authentication is handled by the app's bundle identifier and the configured domain — there is no client-side API key.

For apps using async/await:

try await EzoicAds.shared.initialize(with: config)

Add a Banner Ad

Create an EzoicBannerView, add it to your view hierarchy, and call loadAd() after the SDK is initialized.

import EzoicAdsSDK
import UIKit

class ViewController: UIViewController {
    private var bannerView: EzoicBannerView?

    override func viewDidLoad() {
        super.viewDidLoad()

        let adView = EzoicBannerView(adUnitIdentifier: 12345)
        adView.delegate = self
        adView.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(adView)

        NSLayoutConstraint.activate([
            adView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            adView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            adView.widthAnchor.constraint(equalToConstant: EzoicBannerSize.banner.width)
        ])

        bannerView = adView
        adView.loadAd()
    }
}

extension ViewController: EzoicBannerViewDelegate {
    func bannerViewDidLoad(_ bannerView: EzoicBannerView) {
        print("Banner loaded")
    }

    func bannerView(_ bannerView: EzoicBannerView, didFailToLoadWithError error: EzoicError) {
        print("Banner failed: \(error)")
    }

    func bannerView(_ bannerView: EzoicBannerView, didChangeSize size: CGSize) {
        print("Banner size: \(Int(size.width))x\(Int(size.height))")
    }
}

Replace 12345 with your numeric Ezoic ad unit identifier. The SDK fetches the Google Ad Manager ad unit, Prebid configuration, supported sizes, targeting values, and refresh interval from Ezoic servers. Height comes from the view's intrinsic content size — do not pin a fixed height constraint, or collapse will not take effect.

The delegate also reports bannerViewDidRecordImpression, bannerViewDidRecordClick, bannerViewWillPresentScreen, bannerViewDidDismissScreen, and bannerView(_:didChangeSize:) — all optional.

Unfilled ads

When no ad fills, EzoicBannerView collapses to zero height so it does not leave a blank gap. This is controlled by collapseOnNoFill (default true). If a refresh does not fill, the previous ad stays visible. bannerView(_:didChangeSize:) reports the displayed size, or .zero when the view collapses.

You can load ads with an adaptive default size, a typed size, one size string, or a list of size strings.

adView.loadAd()
adView.loadAd(size: EzoicBannerSize.mediumRectangle)
adView.loadAd(size: "300x250")
adView.loadAd(sizes: ["300x250", "320x50"])

Common sizes include:

  • EzoicBannerSize.banner: 320x50
  • EzoicBannerSize.largeBanner: 320x100
  • EzoicBannerSize.mediumRectangle: 300x250
  • EzoicBannerSize.fullBanner: 468x60
  • EzoicBannerSize.leaderboard: 728x90
  • EzoicBannerSize.custom(width:height:): custom dimensions

You can also use the adaptive size for the current screen width:

let adaptiveSize = EzoicBannerSize.adaptiveSize

Add a Rewarded Ad

Rewarded ads are full-screen ads that grant an in-app reward when the user finishes watching. They use a load-then-show lifecycle: load the ad ahead of time (for example, at the start of a level), then present it at a natural break and grant the reward when it is earned.

import EzoicAdsSDK

var rewardedAd: EzoicRewardedAd?

func loadRewardedAd() {
    EzoicRewardedAd.load(adUnitIdentifier: 12345) { [weak self] result in
        switch result {
        case .success(let ad):
            self?.rewardedAd = ad
            ad.delegate = self
        case .failure(let error):
            print("Rewarded load failed: \(error.localizedDescription)")
        }
    }
}

func showRewardedAd() {
    rewardedAd?.show(from: self) { reward in
        print("Earned \(reward.amount) \(reward.type)")
        grantReward(reward.amount)
    }
}

async/await is also supported:

rewardedAd = try await EzoicRewardedAd.load(adUnitIdentifier: 12345)

Replace 12345 with your numeric Ezoic ad unit identifier. Call show(from:) only after the ad has loaded; showing before the load completes (or after the ad was already shown) reports EzoicError.adNotReady. If you pass nil (or omit from:), the SDK presents from the app's top view controller. Rewarded ads are single-use — load a new ad for each opportunity.

You can pass a reward name when you show the ad. The name appears in your reports.

rewardedAd?.show(from: self, rewardName: "extra life") { reward in
    grantReward(reward.amount)
}

Lifecycle events are delivered through EzoicRewardedAdDelegate. The reward type and amount come from the reward configured on the Google Ad Manager rewarded ad unit.

extension ViewController: EzoicRewardedAdDelegate {
    func rewardedAd(_ rewardedAd: EzoicRewardedAd, userDidEarn reward: EzoicReward) {
        grantReward(reward.amount)
    }

    func rewardedAdDidDismiss(_ rewardedAd: EzoicRewardedAd) {
        print("Rewarded ad closed")
    }
}

The delegate also reports rewardedAdDidPresent, rewardedAd(_:didFailToPresentWithError:), rewardedAdDidRecordImpression, and rewardedAdDidRecordClick. All delegate methods are optional.

Add an Interstitial Ad

Interstitial ads are full-screen ads shown at natural transition points (for example, between levels or screens). Unlike rewarded ads, they grant no reward. They use the same load-then-show lifecycle: load the ad ahead of time, then present it at a natural break.

import EzoicAdsSDK

var interstitialAd: EzoicInterstitialAd?

func loadInterstitialAd() {
    EzoicInterstitialAd.load(adUnitIdentifier: 12345) { [weak self] result in
        switch result {
        case .success(let ad):
            self?.interstitialAd = ad
            ad.delegate = self
        case .failure(let error):
            print("Interstitial load failed: \(error.localizedDescription)")
        }
    }
}

func showInterstitialAd() {
    interstitialAd?.show(from: self)
}

async/await is also supported:

interstitialAd = try await EzoicInterstitialAd.load(adUnitIdentifier: 12345)

Replace 12345 with your numeric Ezoic ad unit identifier. Call show(from:) only after the ad has loaded; showing before the load completes (or after the ad was already shown) reports EzoicError.adNotReady. If you pass nil (or omit from:), the SDK presents from the app's top view controller. Interstitial ads are single-use — load a new ad for each opportunity.

Lifecycle events are delivered through EzoicInterstitialAdDelegate.

extension ViewController: EzoicInterstitialAdDelegate {
    func interstitialAdDidDismiss(_ interstitialAd: EzoicInterstitialAd) {
        print("Interstitial ad closed")
    }
}

The delegate also reports interstitialAdDidPresent, interstitialAd(_:didFailToPresentWithError:), and interstitialAdDidRecordImpression/interstitialAdDidRecordClick. All delegate methods are optional.

Add an Outstream Video Ad

Outstream video ads render inline in your own view hierarchy, like a banner, but with a video creative in an SDK-built player. Create an EzoicOutstreamAdView, set its delegate, add it to your view hierarchy, and call loadAd().

import EzoicAdsSDK

var outstreamAd: EzoicOutstreamAdView?

func loadOutstreamAd() {
    let adView = EzoicOutstreamAdView(adUnitIdentifier: 12345)
    adView.delegate = self
    adContainer.addSubview(adView)
    outstreamAd = adView

    adView.loadAd()
}

Replace 12345 with your numeric Ezoic ad unit identifier. Set delegate before calling loadAd() so the load callback isn't missed. Unlike EzoicBannerView, loadAd() takes no size — the SDK renders at the size configured on the server (640×360 by default). Call destroy() when the hosting view is torn down (e.g. in deinit, or when a cell is recycled) so the underlying ad is released.

Lifecycle events are delivered through EzoicOutstreamAdViewDelegate.

extension ViewController: EzoicOutstreamAdViewDelegate {
    func outstreamViewDidLoad(_ outstreamView: EzoicOutstreamAdView) {
        print("Outstream ad loaded")
    }

    func outstreamView(_ outstreamView: EzoicOutstreamAdView, didFailToLoadWithError error: EzoicError) {
        print("Outstream failed: \(error.localizedDescription)")
    }
}

The delegate also reports outstreamViewDidRecordImpression, outstreamViewDidRecordClick, outstreamViewWillPresentScreen, outstreamViewDidDismissScreen, and outstreamView(_:didChangeSize:) — all optional. EzoicOutstreamAdView collapses on no-fill the same way as EzoicBannerView (collapseOnNoFill, default true).

Add an Instream Video Ad

Instream video ads play inside your own video content. Your app owns the video player and the Google IMA SDK; the Ezoic SDK's job is to build the Google Ad Manager VAST ad-tag URL you hand to IMA. There is no Ezoic view — the deliverable is a URL string, so you retain the EzoicInstreamAd instance yourself. EzoicInstreamAd is a view-less, multi-use controller: unlike rewarded and interstitial ads it is not single-use, so keep the instance and call load(contentUrl:delegate:) again for the next video.

import EzoicAdsSDK
import GoogleInteractiveMediaAds

let instreamAd = EzoicInstreamAd(adUnitId: 12345)

func loadInstreamAd() {
    instreamAd.load(contentUrl: playingVideoUrl, delegate: self)
}

Tag delivery and failures arrive through EzoicInstreamAdDelegate:

extension ViewController: EzoicInstreamAdDelegate {
    func instreamAd(_ instreamAd: EzoicInstreamAd, didReceiveAdTag adTagUrl: String) {
        // Feed the tag to your own IMA ads request.
        let request = IMAAdsRequest(
            adTagUrl: adTagUrl,
            adDisplayContainer: adDisplayContainer,
            contentPlayhead: contentPlayhead,
            userContext: nil
        )
        adsLoader.requestAds(with: request)
    }

    func instreamAd(_ instreamAd: EzoicInstreamAd, didFailToLoadWithError error: EzoicError) {
        print("Instream load failed: \(error.localizedDescription)")
        // Play content without a preroll.
    }
}

Replace 12345 with your numeric Ezoic ad unit identifier. contentUrl (the URL of the video being played) is optional and, when supplied, is added to the ad tag for contextual targeting. On an IMA ad error, call getNextAdTagUrl() to retry the next floor in the waterfall — it returns nil once exhausted. On the IMA STARTED event, call reportImpression() (optionally passing a revenueUsd value) to record the Ezoic impression. Call destroy() when the player is torn down (e.g. in deinit) to cancel any in-flight request; the controller is otherwise reusable across loads.

Add a Native Ad

Native ads deliver ad assets (headline, image, body text, call to action) that you render inside your own view hierarchy instead of a fixed banner or full-screen format, so the ad matches the look and feel of your content.

import EzoicAdsSDK
import GoogleMobileAds

var nativeAd: EzoicNativeAd?

func loadNativeAd() {
    EzoicNativeAd.load(adUnitIdentifier: 12345) { [weak self] result in
        switch result {
        case .success(let ad):
            self?.nativeAd = ad
            ad.delegate = self
            guard let gmaAd = ad.nativeAd else { return }

            // Build a NativeAdView, populate its asset views, then attach the ad.
            let adView = NativeAdView()
            let headline = UILabel()
            headline.text = gmaAd.headline
            adView.headlineView = headline
            adView.addSubview(headline)
            adView.nativeAd = gmaAd

            self?.adContainer.addSubview(adView)
        case .failure(let error):
            print("Native load failed: \(error.localizedDescription)")
        }
    }
}

async/await is also supported:

nativeAd = try await EzoicNativeAd.load(adUnitIdentifier: 12345)

Replace 12345 with your numeric Ezoic ad unit identifier. The SDK returns the underlying Google Mobile Ads NativeAd on ad.nativeAd; populate a NativeAdView with your own styling and assign the ad to its nativeAd property.

Call destroy() when the hosting view is torn down (for example in deinit, or when a table or collection view cell is recycled) so the underlying ad is released.

Lifecycle events are delivered through EzoicNativeAdDelegate. All delegate methods are optional.

extension ViewController: EzoicNativeAdDelegate {
    func nativeAdDidRecordImpression(_ nativeAd: EzoicNativeAd) {
        print("Native ad impression")
    }

    func nativeAdDidRecordClick(_ nativeAd: EzoicNativeAd) {
        print("Native ad clicked")
    }
}

The delegate also reports nativeAdWillPresentScreen and nativeAdDidDismissScreen, which fire when a click opens and closes an overlay such as the ad's destination.

Required App Setup for Ads to Serve

A few configuration steps live in your app project and on your website, not in the SDK. Apple and Google require them, and they directly affect whether — and how well — ads fill. The SDK cannot add them for you (see What the SDK does vs. what you must add), so complete all of the following before testing fill.

1. Google Mobile Ads application ID

Add your Google Mobile Ads application ID to Info.plist. This ID is provided by Ezoic and can be found in your Ezoic dashboard. Use the value assigned to your app unless your Ezoic representative gives you a different one. Also set GADIsAdManagerApp so Google Mobile Ads runs in Ad Manager mode.

<key>GADApplicationIdentifier</key>
<string>ca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXX</string>
<key>GADIsAdManagerApp</key>
<true/>

Without a valid GADApplicationIdentifier, the Google Mobile Ads SDK will not initialize and no ads will serve.

2. App Tracking Transparency (ATT)

ATT has the single biggest impact on fill and CPM. Without it, the IDFA is unavailable (all zeros) and a large share of programmatic demand will not bid or will bid much lower.

Add the tracking usage description to Info.plist. Apple requires this string to live in the app's Info.plist — it cannot be supplied by the SDK because it is shown in the App Store privacy label and the system prompt.

<key>NSUserTrackingUsageDescription</key>
<string>This identifier will be used to deliver personalized ads to you.</string>

The SDK presents the ATT prompt for you. During initialize, when the status is still undetermined, the SDK shows the prompt and waits for the user's decision before starting the ad stack, so the IDFA (if granted) is attached to the first ad request. You only need to supply the NSUserTrackingUsageDescription string above — no extra code.

// The SDK requests ATT (if undetermined) before loading ads.
EzoicAds.shared.initialize(with: config) { _ in
    // SDK ready — banners can load with the IDFA available.
}

If you prefer to drive the ATT flow yourself (for example, to show a pre-prompt or to control its timing), set requestATTBeforeAds: false on EzoicConfiguration and request authorization before initializing the SDK so the IDFA is available on the first ad request:

import AppTrackingTransparency

let config = EzoicConfiguration(domain: "example.com", requestATTBeforeAds: false)
ATTrackingManager.requestTrackingAuthorization { _ in
    EzoicAds.shared.initialize(with: config) { _ in }
}
If you disable the SDK's ATT handling, resolve the prompt before initializing the ad SDK. Initializing first means the first ad requests go out without the IDFA even when the user later grants permission, which lowers fill on the initial screen.

3. SKAdNetwork identifiers

SKAdNetworkItems lets buyers attribute installs when the IDFA is unavailable; missing identifiers suppress demand from buyers that require SKAdNetwork. Apple reads this key only from the app's main Info.plist — it is not aggregated from frameworks or SDKs, so it must be added to your app even though the SDK depends on Google Mobile Ads.

Add the SKAdNetworkItems array with Google's published identifiers (Google's own cstr6suwn9.skadnetwork plus the participating third-party buyers). Copy the current, complete list from Google's Prepare privacy strategies page — Google updates it as buyers are added.

<key>SKAdNetworkItems</key>
<array>
    <dict>
        <key>SKAdNetworkIdentifier</key>
        <string>cstr6suwn9.skadnetwork</string>
    </dict>
    <!-- … plus the remaining identifiers from Google's list … -->
</array>

Identifiers must be lowercase. If you add mediation partners, also include any identifiers those partners require.

4. app-ads.txt on your website

Host an app-ads.txt file at the root of the developer/website domain listed on your app's App Store page (for example, https://example.com/app-ads.txt). It authorizes the buyers and exchanges that may sell your inventory; a missing or incomplete file causes most programmatic demand to be filtered out. Ezoic provides the required entries — confirm the file is published and current.

What the SDK does vs. what you must add

Item Provided automatically by the SDK You must add
Google Mobile Ads + Prebid SDKs Yes (transitive dependencies) —
Ad unit, sizes, targeting, Prebid config Yes (fetched from Ezoic) —
GADApplicationIdentifier / GADIsAdManagerApp No (per-app value) Yes, in Info.plist
ATT prompt (request + wait before ads) Yes (SDK presents it during initialize) —
NSUserTrackingUsageDescription string No (App Store privacy requirement) Yes, in Info.plist
SKAdNetworkItems No (Apple reads app Info.plist only) Yes, in Info.plist
app-ads.txt No (hosted on your domain) Yes, on your website

Apple and Google deliberately require these to live in the app bundle or on the publisher domain, so they cannot be bundled inside the SDK. Keep them in mind whenever you ship a new app.

Starting with SDK 1.13.0, the SDK includes an IAB TCF 2.4 consent management platform (CMP ID 299). It only acts for users in GDPR regions; elsewhere no dialog is shown and ads load as before.

The dialog is used only when both of these are true:

  • Ezoic has enabled the consent dialog for your domain. This is a server-side setting.
  • cmpEnabled is true in EzoicConfiguration. This is the default.
If your app already runs another CMP (for example UMP or OneTrust), you must set cmpEnabled: false. See Using your own CMP.

Call presentConsentIfRequired once per process from your first view controller's viewDidAppear, right after EzoicAds.shared.initialize. It is safe to call before initialization finishes: it waits for the init response. Calling it again is harmless: you get .alreadyPresenting while a dialog is in flight and .alreadyDecided once a valid decision is stored.

You must also add a "Privacy settings" button or menu item that is always reachable and calls presentConsentSettings. TCF policy requires users to be able to reopen the dialog and withdraw consent at any time.

import EzoicAdsSDK

private static var consentRequested = false // once per process

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    guard !Self.consentRequested else { return }
    Self.consentRequested = true
    EzoicAds.shared.presentConsentIfRequired(from: self) { outcome in
        switch outcome {
        case .decided(let decision):
            print("User chose \(decision)")
        case .failed(let error):
            print("Consent UI failed: \(error.localizedDescription)")
        default:
            break // .notRequired, .alreadyDecided, .dismissed, .alreadyPresenting
        }
    }
}

// "Privacy settings" button (required): reopen the dialog with the user's stored choices
@objc func privacySettingsTapped() {
    EzoicAds.shared.presentConsentSettings(from: self) { outcome in /* ... */ }
}

async/await is also supported:

let outcome = await EzoicAds.shared.presentConsentIfRequired(from: self)
Upgrading to 1.13.0: if you keep the default cmpEnabled: true and never call presentConsentIfRequired, ad loads for GDPR-region users will fail once Ezoic enables the consent dialog for your domain. Either add the call above or set cmpEnabled: false if you run your own CMP.

Completions run on the main thread. When the dialog was shown, the outcome is delivered after it has finished dismissing, so you can present your next screen from the completion. ConsentOutcome tells you what happened:

Outcome When
.notRequired GDPR doesn't apply, the built-in CMP is disabled (cmpEnabled: false or by Ezoic), another CMP owns consent, or consent is managed by the app (see Setting consent manually)
.alreadyDecided A still-valid decision is stored; no dialog shown
.decided(decision) The user chose .acceptAll, .rejectAll or .custom; the choice is saved
.dismissed The dialog closed without a choice; ads stay gated for this session. This can happen without user action (the dialog failed to start, or resetConsent() ran while it was loading or open)
.alreadyPresenting A consent dialog is already on screen, or one is being prepared with nothing on screen yet
.failed(error) The dialog couldn't be shown (for example a network error, or no view controller in a window within 10 seconds). .failed(.notInitialized) means it was called before initialize, or init failed

Other consent calls:

  • presentConsentSettings always reopens the dialog in GDPR regions, even if the user already decided. It returns .notRequired outside GDPR regions, when cmpEnabled is false, when another CMP is present, or when consent is managed by the app. If the dialog can't be loaded (for example offline) and a still-valid decision is stored, it returns .alreadyDecided.
  • isConsentRequired (Bool?) is nil until the init request completes, or when the server sent no consent information. It is true whenever GDPR applies and the built-in CMP is in charge, including after the user has decided, and false otherwise.
  • resetConsent() deletes the stored decision so the dialog shows again. Ads are gated again until the user decides. Keys written by another CMP are left alone.

Decisions are written as standard IABTCF_* keys to UserDefaults.standard, with the Google Additional Consent string in IABTCF_AddtlConsent, so Prebid, Google Ad Manager, and other IAB-aware SDKs read them as usual. A stored decision is asked for again after the publisher's consent interval (at most 390 days), when the TCF policy version changes, or when the vendor list moves more than 2 versions ahead.

In GDPR regions, while the built-in CMP is in charge and no decision is stored, ad loads wait for the user's decision:

  • While the consent dialog is loading or on screen, ad loads wait up to 5 minutes in total per dialog, counted from presentConsentIfRequired and shared by all waiting ad loads.
  • While no dialog is in progress, the app is in the background, or the dialog has been replaced by a full-screen presentation, ad loads wait up to 10 seconds.

If the wait runs out, the ad load fails with EzoicError.consentRequired. iOS can't tell when the dialog is only covered by a sheet or an alert, so ad loads keep waiting in that case, up to the 5-minute cap.

If the dialog can't be shown (.failed), the SDK fails open: ads proceed with IABTCF_gdprApplies=1 and no TC string, which Google treats as limited ads and many Prebid bidders skip. An earlier TC string, if any, is cleared when the stored decision was stale. .failed(.notInitialized) writes nothing.

Using your own CMP

Set cmpEnabled: false. The SDK then reads your CMP's IABTCF_* keys as before. The built-in CMP also stays out of the way automatically if it finds IABTCF_CmpSdkID set to another CMP's ID.

let config = EzoicConfiguration(domain: "example.com", cmpEnabled: false)

Whichever CMP writes them, the SDK automatically reads consent signals from UserDefaults:

  • TCF v2 consent is read from IABTCF_* keys.
  • GPP consent is read from IABGPP_* keys.
  • US Privacy consent is read from the standard IAB privacy key.

You can also set consent in code. Manual consent and the built-in CMP are mutually exclusive: once setGDPRConsent is called, or when autoReadConsent is false, the built-in CMP doesn't gate ad loads, show its dialog, or write IABTCF_* keys, and resetConsent() leaves the keys alone.

The setGDPRConsent value lasts for the current process only. Call setGDPRConsent before EzoicAds.shared.initialize on every launch, or set cmpEnabled: false. Otherwise each cold start begins with the built-in CMP in charge until your call lands, and it can gate ad loads, show its dialog, and write IABTCF_* keys. setGDPRConsent doesn't write your consent to IABTCF_* keys in UserDefaults for Google Mobile Ads or other SDKs in the app, so your own CMP must write those keys.

EzoicAds.shared.setGDPRConsent(
    applies: true,
    consentString: "TCF_CONSENT_STRING"
)

EzoicAds.shared.setGPPConsent(
    gppString: "GPP_STRING",
    sectionIds: "7"
)

EzoicAds.shared.setSubjectToCOPPA(true)

// Then initialize
EzoicAds.shared.initialize(with: config) { result in /* ... */ }

Pageview Tracking

Ezoic reports app traffic per screen, the way it reports a website per URL. Apps have no URLs, so the SDK gives each screen one:

https://<your domain>/<bundle identifier>/<screen label>

That URL is what per-page reporting and placement optimization key on, so the screen label is what you will see in the Ezoic dashboard.

Automatic tracking

The SDK automatically tracks a pageview for every UIViewController that appears after initialization (container and system controllers are excluded). Without any code from you, the screen label defaults to the view controller's class name, for example https://example.com/com.example.app/SettingsViewController.

Labeling screens

To name screens the way you want to see them in reporting, call trackPageview(screen:) when the user lands on a screen, typically from viewDidAppear. Available in SDK 1.12.0 and later.

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    EzoicAds.shared.trackPageview(screen: "members area")
    // reported as https://example.com/com.example.app/members-area
}

For apps using async/await:

let success = await EzoicAds.shared.trackPageview(screen: "members/profile")
// reported as https://example.com/com.example.app/members/profile

Labels are free text. Use / for hierarchy (members/profile); spaces become -, punctuation is dropped, and case is kept. The label is also attached to every ad request on that screen, so impressions and pageviews report under the same page.

A labeled trackPageview(screen:) takes precedence over the automatic pageview for the same navigation, so you can label only the screens you care about without double counting. Calling trackPageview() with no label still records a pageview under the current screen label:

EzoicAds.shared.trackPageview { success in
    print("Pageview tracked: \(success)")
}

Manual tracking only

If you would rather the SDK never guess, disable automatic tracking and label every screen yourself:

let config = EzoicConfiguration(
    domain: "example.com",
    autoTrackPageviews: false
)

Troubleshooting

SDK Not Initializing

  1. Confirm the configured domain matches your Ezoic dashboard.
  2. Confirm the app has network access.
  3. Enable debugEnabled: true and review SDK logs in the Xcode console.

Ads Not Loading

  1. Initialize the SDK before calling loadAd().
  2. Confirm the Ezoic ad unit identifier is configured in Ezoic.
  3. Confirm the Google Mobile Ads application ID is present in Info.plist.
  4. Check consent configuration if traffic is subject to privacy regulations.
  5. If ad loads fail with EzoicError.consentRequired, the built-in consent dialog is waiting for a decision. Call presentConsentIfRequired after initializing, or set cmpEnabled: false if you run your own CMP. See Built-in consent dialog.