iOS Integration Guide

Integrate AppSpike into your iOS app and serve your first ad in under 5 minutes.

AppSpike sits on top of your existing ad networks and uses real-time bidding to route every impression to the highest-paying demand source. You keep your current ad network accounts — AppSpike optimizes across all of them.

Prerequisites

Step 1: Add the SDK

Once you are approved for early access, AppSpike will send you the SDK along with setup instructions for your project.

Step 2: Configure Info.plist

Add the following keys to your Info.plist for App Tracking Transparency and SKAdNetwork support:

Info.plist
<key>NSUserTrackingUsageDescription</key>
<string>This allows us to show you more relevant ads.</string>

<key>SKAdNetworkItems</key>
<array>
  <!-- AppSpike SKAdNetwork ID -->
  <dict>
    <key>SKAdNetworkIdentifier</key>
    <string>appspike123.skadnetwork</string>
  </dict>
  <!-- Add each ad network's SKAdNetwork ID -->
  <!-- See full list: https://docs.appspike.dev/ios/skadnetwork -->
</array>

Step 3: Request App Tracking Transparency

Request ATT authorization before initializing the SDK to maximize ad revenue:

import AppTrackingTransparency

func requestTrackingPermission(completion: @escaping () -> Void) {
    if #available(iOS 14, *) {
        ATTrackingManager.requestTrackingAuthorization { status in
            DispatchQueue.main.async {
                completion()
            }
        }
    } else {
        completion()
    }
}

Step 4: Initialize the SDK

AppSpike is a Kotlin Multiplatform SDK. In Swift, access the singleton via AppSpike.shared. Pass your API key and the ad network plugins you want to mediate:

AppDelegate.swift
import AppSpikeSDK

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {

        AppSpike.shared.initialize(
            apiKey: "YOUR_API_KEY",
            plugins: [AdMobPlugin.shared, IronSourcePlugin.shared],
            testMode: false
        ) { result in
            if result is InitResult.Success {
                print("AppSpike initialized")
            } else if let error = result as? InitResult.Error {
                print("AppSpike init failed: \(error.message)")
            }
        }

        return true
    }
}

Banners are loaded asynchronously. After loading, the BannerAd provides a bannerView (UIView) to add to your view hierarchy:

MainViewController.swift
import AppSpikeSDK

class MainViewController: UIViewController {
    private var bannerAd: BannerAd?

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        bannerAd?.resume()

        AppSpike.shared.loadBanner(placementId: "YOUR_PLACEMENT_ID") { [weak self] result in
            guard let self = self else { return }
            let ad = result?.getOrNull() as? BannerAd
            if let ad = ad {
                self.bannerAd?.bannerView.removeFromSuperview()
                self.bannerAd?.destroy()
                self.bannerAd = ad
                ad.initialize()
                let bannerView = ad.bannerView
                self.view.addSubview(bannerView)
                bannerView.translatesAutoresizingMaskIntoConstraints = false
                NSLayoutConstraint.activate([
                    bannerView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor),
                    bannerView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor)
                ])
            } else if let error = result?.exceptionOrNull() {
                print("Banner failed: \(error)")
            }
        }
    }

    override func viewWillDisappear(_ animated: Bool) {
        bannerAd?.pause()
        super.viewWillDisappear(animated)
    }

    deinit {
        bannerAd?.destroy()
    }
}

Step 6: Display an Interstitial Ad

GameViewController.swift
import AppSpikeSDK

class GameViewController: UIViewController {
    private var interstitialAd: InterstitialAd?

    override func viewDidLoad() {
        super.viewDidLoad()
        loadInterstitial()
    }

    private func loadInterstitial() {
        AppSpike.shared.loadInterstitial(placementId: "YOUR_PLACEMENT_ID") { [weak self] result in
            if let ad = result?.getOrNull() as? InterstitialAd {
                self?.interstitialAd = ad
            } else if let error = result?.exceptionOrNull() {
                print("Interstitial failed to load: \(error)")
            }
        }
    }

    // Call this at a natural break point (e.g., between levels)
    func showInterstitial() {
        guard let ad = interstitialAd else {
            print("Interstitial not ready yet")
            return
        }

        ad.show()
        interstitialAd = nil
        loadInterstitial()
    }
}

Step 7: Display a Rewarded Video Ad

Rewarded video ads use a reactive stream to deliver events. Subscribe to the observable returned by show() to track reward delivery:

StoreViewController.swift
import AppSpikeSDK

class StoreViewController: UIViewController {
    private var rewardedVideo: RewardedVideo?

    override func viewDidLoad() {
        super.viewDidLoad()
        loadRewarded()
    }

    private func loadRewarded() {
        AppSpike.shared.loadRewardedVideo(placementId: "YOUR_PLACEMENT_ID") { [weak self] result in
            if let video = result?.getOrNull() as? RewardedVideo {
                self?.rewardedVideo = video
            } else if let error = result?.exceptionOrNull() {
                print("Rewarded video failed to load: \(error)")
            }
        }
    }

    // Call this when the user opts in (e.g., "Watch ad for 50 coins")
    func showRewarded() {
        guard let video = rewardedVideo else {
            print("Rewarded video not ready yet")
            return
        }

        video.show(userId: nil, serverVerificationPayload: nil).subscribe(
            onNext: { [weak self] event in
                if event is RewardedVideoEvent.RewardReceived {
                    print("User earned reward")
                    self?.grantReward()
                } else if event is RewardedVideoEvent.ClosedWithReward {
                    print("Ad closed with reward")
                } else if event is RewardedVideoEvent.ClosedEarly {
                    print("User closed ad early")
                } else if let error = event as? RewardedVideoEvent.DisplayError {
                    print("Display error: \(error.message)")
                }
            },
            onComplete: { [weak self] in
                self?.rewardedVideo = nil
                self?.loadRewarded()
            },
            onError: { error in
                print("Rewarded error: \(error)")
            }
        )
    }

    private func grantReward() {
        // Your reward logic here
    }
}

Step 8: Native Ads

Native ads use a view binder pattern. On iOS, the binder maps UIView subclasses to ad content slots:

FeedViewController.swift
import AppSpikeSDK

class FeedViewController: UIViewController {
    private var nativeAd: NativeAd?
    private var nativeAdView: UIView?

    @IBOutlet weak var adContainerView: UIView!
    @IBOutlet weak var headlineLabel: UILabel!
    @IBOutlet weak var bodyLabel: UILabel!
    @IBOutlet weak var ctaButton: UIButton!
    @IBOutlet weak var iconImageView: UIImageView!
    @IBOutlet weak var mediaView: UIView!
    @IBOutlet weak var optionsView: UIView!

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        nativeAd?.resume()

        AppSpike.shared.loadNativeAd(placementId: "YOUR_PLACEMENT_ID") { [weak self] result in
            guard let self = self else { return }
            if let ad = result?.getOrNull() as? NativeAd {
                self.nativeAdView?.removeFromSuperview()
                self.nativeAd?.destroy()
                self.nativeAd = ad

                let binder = ad.binder()
                binder.setHeadlineView(view: self.headlineLabel)
                binder.setBodyView(view: self.bodyLabel)
                binder.setCallToActionView(view: self.ctaButton)
                binder.setIconView(view: self.iconImageView)
                binder.setMediaContentView(view: self.mediaView)
                binder.setOptionsView(view: self.optionsView)

                let adView = binder.bind()
                ad.initialize()
                self.nativeAdView = adView
                self.adContainerView.addSubview(adView)
            } else if let error = result?.exceptionOrNull() {
                print("Native ad failed: \(error)")
            }
        }
    }

    override func viewWillDisappear(_ animated: Bool) {
        nativeAd?.pause()
        super.viewWillDisappear(animated)
    }

    deinit {
        nativeAd?.destroy()
    }
}

Step 9: Rewarded Interstitial Ads

Rewarded interstitials combine the full-screen experience of an interstitial with a reward. They follow the same reactive pattern as rewarded video ads:

LevelViewController.swift
import AppSpikeSDK

class LevelViewController: UIViewController {
    private var rewardedInterstitial: RewardedInterstitial?

    override func viewDidLoad() {
        super.viewDidLoad()
        loadRewardedInterstitial()
    }

    private func loadRewardedInterstitial() {
        AppSpike.shared.loadRewardedInterstitial(placementId: "YOUR_PLACEMENT_ID") { [weak self] result in
            if let ad = result?.getOrNull() as? RewardedInterstitial {
                self?.rewardedInterstitial = ad
            } else if let error = result?.exceptionOrNull() {
                print("Rewarded interstitial failed to load: \(error)")
            }
        }
    }

    func showRewardedInterstitial() {
        guard let ad = rewardedInterstitial else {
            print("Rewarded interstitial not ready yet")
            return
        }

        ad.show(userId: nil, serverVerificationPayload: nil).subscribe(
            onNext: { [weak self] event in
                if event is RewardedVideoEvent.RewardReceived {
                    self?.grantReward()
                } else if let error = event as? RewardedVideoEvent.DisplayError {
                    print("Display error: \(error.message)")
                }
            },
            onComplete: { [weak self] in
                self?.rewardedInterstitial = nil
                self?.loadRewardedInterstitial()
            },
            onError: { error in
                print("Error: \(error)")
            }
        )
    }

    private func grantReward() {
        // Your reward logic here
    }
}

Testing

Enable test mode during development to avoid serving live ads:

AppSpike.shared.initialize(
    apiKey: "YOUR_API_KEY",
    plugins: [AdMobPlugin.shared, IronSourcePlugin.shared],
    testMode: true
) { result in
    // Test ads will always fill and never charge
}

Best Practices

Next Steps