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
- Xcode 15.0 or later
- iOS 15.0+ deployment target
- Swift 5.9+
- An AppSpike account and API key (sign up)
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:
<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:
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
}
}
Step 5: Display a Banner Ad
Banners are loaded asynchronously. After loading, the BannerAd provides a bannerView (UIView) to add to your view hierarchy:
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.swiftimport 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:
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:
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.swiftimport 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
- Pre-load interstitials and rewarded ads as early as possible, then show them at natural break points. Never interrupt the user mid-action.
- Always reload after showing. Each ad object is single-use — load the next one after the previous one completes.
- Request ATT before initializing. Calling
ATTrackingManager.requestTrackingAuthorizationbefore SDK init maximizes bid values from demand sources. - Ship with all available ad network plugins. More demand sources means higher competition and higher eCPMs. AppSpike's mediation handles the rest.
- Use test mode during development. Live impressions from test devices waste ad budget and can flag your account.
- Don't gate core functionality behind rewarded ads. Use them for optional bonuses (extra lives, cosmetics, currency). Apple may reject apps that require ads to function.
- Add all required SKAdNetwork IDs. Missing IDs reduce attribution and can lower bid values from networks that rely on SKAdNetwork.