Skip to content

Getting Started

This page is the whole integration: install, configure, load, show, style, observe, and clean up. The API Reference has the exhaustive detail on every type mentioned here.

Two Elo ad placements in a chat app: an in-chat card after the assistant message, and a below-input card under the composer

The SDK renders two placements: a card inline in the chat list (Show it) and a banner strip above the keyboard (Keyboard banner).

Add the SDK to your project via Swift Package Manager. In Xcode: File → Add Package Dependencies and enter:

https://github.com/growlads/elo-ios-sdk

After Xcode resolves the package, make sure the EloAds library product is added to the same app target that will import EloAds. Adding the repository alone does not make the module visible to your target.

Or add it directly to your Package.swift:

Package.swift
dependencies: [
.package(url: "https://github.com/growlads/elo-ios-sdk", exact: "0.7.0"),
],
targets: [
.target(
name: "MyApp",
dependencies: [
.product(name: "EloAds", package: "elo-ios-sdk"),
],
),
]

Requires iOS 16.0+, Swift 5.9+, and Xcode 15+.

Call this once, early in your app lifecycle:

MyApp.swift
import SwiftUI
import EloAds
@main
struct MyApp: App {
init() {
Elo.configure(
publisherId: "YOUR_PUBLISHER_ID",
adUnitId: "YOUR_AD_UNIT_ID",
)
}
var body: some Scene {
WindowGroup { ContentView() }
}
}

Good to know at this point:

  • Passive geo sharing is on by default. If your app already holds a location permission, the SDK attaches a coarse, rounded location to ad requests. It never prompts for the permission itself. Opt out with shareGeoLocation: false here or Elo.setShareGeoLocation(false) at runtime — see Privacy & Consent.
  • Child-directed apps must set the coppa / tfua flags on EloConfiguration — see Privacy & Consent.
  • COPPA/TFUA and log level go through the full form Elo.configure(with:) instead — see EloConfiguration.

Pass the recent chat history to get a contextual ad. loadAd is async, safe to call from any task, and returns an AdResult:

let messages = [
ChatMessage(role: .user, content: "Best laptops for coding?"),
ChatMessage(role: .assistant, content: "I'd recommend the MacBook Pro M4..."),
]
let result = await Elo.loadAd(messages: messages)
  • Human-to-human chats use the same two roles: map the device owner to .user and the other participant to .assistant — see MessageRole.
  • Long conversations: send a .summary message followed by the most recent user/assistant exchange instead of the full history — either shape works.
  • Message ids and times: pass the id and createdAt your app keeps for each turn, the same values on every request, so Elo knows when each turn happened and can match the turn to a Search API call that sends the same id. A .summary message carries the id and time of the turn it summarizes. See ChatMessage.
  • Your own message type can conform to MessageRepresentable instead of mapping by hand.
  • Extra targeting: pass contextObjects: (screen/page metadata) when the surrounding UI carries useful signal.
  • Preloading: if a slot is about to become visible, warm the cache with fire-and-forget Elo.preloadAd(messages: messages, maxHeight: maxHeight); a subsequent loadAd with identical inputs may return immediately. The cache key is the messages, the context objects, maxHeight and maxWidth, and the consent snapshot, so pass the same slot size to the preload and to the load that uses it.

EloAdView renders the ad and handles render, impression, and click tracking automatically (impressions fire after ≥50% visibility for 1 continuous second, with the app frontmost and nothing drawn over the ad). Pass the AdResult and the view collapses to nothing on no-fill or error:

EloAdView(result: result)

In a chat list, keep the ad view inline with the messages and re-fetch after each assistant turn:

ChatView.swift
struct ChatView: View {
@State private var messages: [ChatMessage] = []
@State private var adResult: AdResult?
var body: some View {
ScrollView {
LazyVStack(spacing: 12) {
ForEach(messages.indices, id: \.self) { index in
MessageBubble(message: messages[index])
}
EloAdView(result: adResult)
.padding(.horizontal)
}
}
.task(id: messages.count) {
guard messages.last?.role == .assistant else { return }
adResult = await Elo.loadAd(messages: messages)
}
}
}

Elo picks the ad’s format from the space you have. Without maxHeight, as above, an ad between messages is a card. Pass maxHeight: to Elo.loadAd or a request-owning EloAdView for a smaller slot; see Ad Formats.

One modifier at the screen level pins a banner strip above the keyboard — no keyboard tracking or layout code:

ChatScreen()
.eloKeyboardBannerAd(messages: messages)

The slot requests the strip’s height, renders a strip on fill, and collapses on no-fill or error. When the keyboard is hidden it rests at the bottom safe area. Changing messages reloads the slot; use the optional onResult: closure to observe outcomes. While the request is in flight it shows a strip-shaped skeleton at the slot’s own size, so the composer above keeps its place when an ad arrives; pass showLoadingPlaceholder: false to keep the keyboard edge clear until an ad fills.

Two caveats:

  • The modifier rides SwiftUI’s keyboard safe area. If the view it’s applied to (or an ancestor) uses .ignoresSafeArea(.keyboard), the keyboard will cover the banner. Apply it outside any safeAreaInset(edge: .bottom) your composer uses.
  • Keep your composer’s hit area from extending into the gap above the strip — ad networks (AdMob included) treat placements that invite accidental clicks next to text inputs as policy violations.

.eloKeyboardBannerAd is SwiftUI-only. UIKit apps host a request-owning EloAdView with a maxHeight of the strip’s height in a UIHostingController pinned to view.keyboardLayoutGuide.topAnchor.

By default the ad view adapts to the system color scheme. Override any subset with eloAdStyle — on one view or a whole subtree:

ChatListView()
.eloAdStyle(.init(borderColor: .gray, borderWidth: 1, cornerRadius: 16))

Beyond colors, the style sets the typeface, forces light or dark colors on the ad alone, and can drop the ad’s own surface so it sits inside your chat row:

ChatListView()
.eloAdStyle(.init(
callToActionBackground: .yellow, // the button label turns black to stay readable
font: .custom("Inter"),
colorScheme: .dark,
chrome: .bare
))
  • font draws the headline, description, button label, and disclosure badge in your typeface (.system(design: .rounded) or .custom("YourFont")). The SDK keeps its own sizes and weights, so the ad stays the height its slot asked for.
  • colorScheme forces .light or .dark colors on the ad only, for a chat screen that doesn’t follow the device setting.
  • chrome: .bare removes the background, border, and rounded corners. The card also drops its inner padding, except 4 points at the top and trailing edge for the disclosure badge. The strip keeps its padding, because the padding holds its tap inset.
  • Set only callToActionBackground and the button label turns black or white, whichever contrasts more with your color.

Localize the disclosure badge per view. A string literal converts to an EloAdDisclosure implicitly; a String variable, including anything from NSLocalizedString, needs the explicit wrap:

EloAdView(
result: adResult,
sponsoredLabel: "Werbung",
)
EloAdView(
result: adResult,
sponsoredLabel: EloAdDisclosure(NSLocalizedString("ad.sponsored", comment: "")),
)

EloAdDisclosure also carries the badge’s font, color, and an accessibilityIdentifier for UI tests.

All style properties are listed under EloAdStyle.

Register a delegate to observe load, fail, impression, and click events. The SDK holds it weakly (retain it yourself) and calls back on the main actor; all methods have default no-op implementations:

@MainActor
final class AdObserver: EloAdDelegate {
func eloAdDidLoad(_ ad: EloAd) { /* analytics */ }
func eloAdDidFailToLoad(error: EloError) { /* logging */ }
func eloAdDidTrackImpression(_ ad: EloAd) { /* analytics */ }
func eloAdDidReceiveClick(_ ad: EloAd) { /* analytics */ }
}
let observer = AdObserver()
Elo.setDelegate(observer)

By default a tap on an ad opens its click URL in the system browser. To use your own in-app browser, such as an SFSafariViewController, open the URL from your delegate’s eloAdOpenClickURL(_:url:) and return true, and report when the browser opens and closes. The SDK sends how long the user stayed on the advertiser’s page.

func eloAdOpenClickURL(_ ad: EloAd, url: URL) -> Bool {
Elo.trackBrowserOpened(ad)
browser.open(url) { Elo.trackBrowserClosed(ad) }
return true
}

Return false to let the SDK open the URL in the system browser. In an app, an openURL action you set around EloAdView does not change where ads open; in an app extension, which has no UIApplication, the SDK opens ads through that action. Do not open the URL from eloAdDidReceiveClick: the SDK opens it too, so the ad would open twice.

The time counts any time the app spent in the background while the browser was open. A second open before the close, and a close without an open, are ignored.

Elo is a single demand source. If you also run another ad network, keep the fallback in your own code: request Elo first, render the ad on .loaded, and hand the same slot to your other network on .noFill or .error. Both non-loaded cases mean Elo has nothing to show for this request, so treat them the same way.

switch await Elo.loadAd(messages: messages) {
case .loaded(let ad, _):
slot = .elo(ad) // e.g. EloAdView(ad: ad)
case .noFill, .error:
slot = .otherNetwork // your other network fills the same slot
}

In a chat UI you can request twice for one slot: once when the user’s message arrives, and again with the assistant’s answer appended if the first request did not fill. The second request carries more context, so it can fill where the first did not.

var result = await Elo.loadAd(messages: messages) // ends with the user's message
if case .loaded = result {} else {
result = await Elo.loadAd(messages: messages + [answer])
}

AdResult.loaded also carries eCpm, the price the Elo server quoted for the fill. When your other network quotes a price at load time, request both and render the higher one, calling EloAd.releaseResources() on the Elo ad you discard. Only call it when you’ve decided not to render the ad; ads passed to EloAdView are cleaned up by the SDK automatically. See Other Ad Networks for the comparison patterns.

  • Elo.disable() pauses ad requests without tearing down the SDK (e.g. while a paid user is signed in) — loadAd returns .noFill(.sessionDisabled) until Elo.enable().
  • Elo.shutdown() cancels in-flight requests and releases resources; after it, loadAd returns .noFill(.notConfigured) until you configure again.
  • Example app — a runnable chat app demonstrating chat-list ads, the keyboard banner, styling, and the diagnostics screen
  • Other Ad Networks — run Elo alongside another network with a fallback in your own code
  • API Reference — complete reference for all public types
  • Release Notes — what changed per version, with upgrade callouts
  • Privacy & Consent — CMP setup, COPPA/TFUA, geo sharing, and your App Store disclosures
  • app-ads.txt — declare Elo as an authorized seller of your inventory