The official Adhese SDK for native iOS. Fetch ads from the Adhese API with async/await and render them with a UIKit view or a SwiftUI view. Impression and viewable-impression tracking are handled for you.
- Swift-first,
async/await,Sendablevalue types - UIKit (
AdView) and SwiftUI (AdBanner) rendering - Managed slot loading with optional lazy loading and auto-refresh
- IAB-compliant viewability (50% of pixels visible for 1 second)
- Creative sandboxing — auto-redirecting creatives cannot hijack your app
- No global state — create one client per account and reuse it
- iOS 15+, distributed via Swift Package Manager and CocoaPods
Upgrading from the 1.x Objective-C SDK? The API changed completely. See MIGRATION.md.
In Xcode: File > Add Package Dependencies… and enter the repository URL, or add
it to your Package.swift:
dependencies: [
.package(url: "https://github.com/adswag/avi.git", from: "2.0.0")
]Then add Adhese to your target's dependencies.
pod 'Adhese', '~> 2.0'Create one Adhese client for your account and reuse it. Fetch ads for one or
more slots, then render each ad.
import Adhese
let adhese = Adhese(account: "your-account")
let request = AdRequest(slots: [
.init(location: "homepage", format: "300x250"),
.init(location: "homepage", format: "300x250", identifier: "2"),
])
do {
let ads = try await adhese.requestAds(request)
// Render `ads` (see below). May be empty when nothing is booked.
} catch {
print("Ad request failed: \(error.localizedDescription)")
}Use AdheseConfiguration for anything beyond the account name:
var config = AdheseConfiguration(account: "your-account")
config.consent = .all // consent signal (see "Consent" below)
config.timeout = 5 // seconds; default is 10
config.isDebugLoggingEnabled = true
let adhese = Adhese(configuration: config)| Option | Type | Default | Description |
|---|---|---|---|
account |
String |
— | Your Adhese account name. Required, non-empty. |
consent |
Consent |
.none |
The consent signal sent with every request. |
timeout |
TimeInterval |
10 |
Per-request timeout in seconds. |
isDebugLoggingEnabled |
Bool |
false |
Verbose logging via os.Logger (subsystem com.adhese.sdk). |
customBaseURL |
URL? |
nil |
Host override for testing/staging. |
There are two ways to fill a view with an ad:
- Managed loading (recommended): hand the view a slot and a client; it fetches and renders on its own and can lazy-load and auto-refresh.
- Manual loading: fetch ads yourself with
requestAds(_:)and hand one to the view — useful when one request fills several slots or you need theAddata before rendering.
Managed:
import SwiftUI
import Adhese
struct ContentView: View {
private let adhese = Adhese(account: "your-account")
var body: some View {
AdBanner(
slot: .init(location: "homepage", format: "300x250"),
using: adhese
)
.onEvent { event in
if case .failed(let error) = event { print(error) }
}
.frame(height: 250)
}
}Manual, filling multiple slots from one request:
struct ContentView: View {
@State private var ads: [Ad] = []
private let adhese = Adhese(account: "your-account")
var body: some View {
VStack {
ForEach(ads) { ad in
AdBanner(ad: ad)
.frame(height: CGFloat(ad.height ?? 250))
}
}
.task {
ads = (try? await adhese.requestAds(
AdRequest(slots: [.init(location: "homepage", format: "300x250")])
)) ?? []
}
}
}Give the banner an explicit size with .frame(...); the creative scales to fit.
onEvent delivers AdBannerEvent values: .rendered, .failed(AdheseError),
.impressionRecorded, .viewableImpressionRecorded.
Add an AdView, constrain it, set a delegate, and load a slot (managed) or an
Ad you fetched yourself (manual):
import Adhese
final class MyViewController: UIViewController, AdViewDelegate {
private let adhese = Adhese(account: "your-account")
private let adView = AdView()
override func viewDidLoad() {
super.viewDidLoad()
adView.delegate = self
adView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(adView)
NSLayoutConstraint.activate([
adView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
adView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
adView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
adView.heightAnchor.constraint(equalToConstant: 250),
])
// Managed: the view fetches and renders the slot itself.
adView.load(
slot: .init(location: "homepage", format: "300x250"),
using: adhese
)
}
}The manual equivalent is adView.load(ad) with an Ad from requestAds(_:).
Managed loading accepts AdLoadingOptions:
adView.load(
slot: .init(location: "article", format: "300x250"),
using: adhese,
options: AdLoadingOptions(
lazyLoadingMargin: UIScreen.main.bounds.height, // fetch one screen early
refreshInterval: 60 // refresh after 60s viewable
)
)| Option | Default | Description |
|---|---|---|
lazyLoadingMargin |
nil (immediate) |
Defer the ad request until the view is within this many points of the screen edge, so offscreen slots don't cost impressions. |
refreshInterval |
nil (never) |
Re-request and re-render the slot after this many seconds of accumulated viewable time (at least 50% visible while the app is active). Clamped to a 10-second minimum; most accounts should stay at 30+ seconds. |
Render and impression callbacks fire again on every refresh. Call
adView.cancelManagedLoading() to stop; the current ad stays visible. The same
options are available on AdBanner(slot:using:parameters:options:).
All AdViewDelegate methods are optional:
func adViewDidRender(_ adView: AdView) // creative rendered
func adView(_ adView: AdView, didFailWith error: AdheseError) // load/render failed
func adViewDidRecordImpression(_ adView: AdView) // impression pixel fired
func adViewDidRecordViewableImpression(_ adView: AdView) // viewable impression fired
func adView(_ adView: AdView, shouldOpen url: URL) -> Bool // return false to handle clicks yourselfBy default, taps open the click-through URL in the system browser. To handle it
yourself (e.g. an in-app browser), either set adView.opensClicksAutomatically = false
or return false from adView(_:shouldOpen:):
func adView(_ adView: AdView, shouldOpen url: URL) -> Bool {
presentInAppBrowser(url)
return false // suppress automatic opening
}A rendered creative can never navigate the ad container itself. Link taps and JavaScript navigations that immediately follow a touch are treated as click-throughs and routed through the click handling above; automatic redirects without a user gesture — a common malvertising pattern — are blocked and logged. Content inside the creative's own iframes is unaffected.
Every slot is identified on the Adhese side by a name composed as
{location}{identifier}-{format}:
| Property | Required | Example | Description |
|---|---|---|---|
location |
Yes | "homepage" |
The page/app-state identifier configured in your account. |
format |
Yes | "300x250" |
The format code configured in your account. |
identifier |
No | "bar" |
Appended to the location, e.g. to differentiate duplicate slots. |
So Slot(location: "foo", format: "300x250", identifier: "bar") resolves to the
slot name foobar-300x250, available as slot.slotName. Slots in one request are
independent — mixing locations is fine:
let request = AdRequest(slots: [
.init(location: "homepage", format: "300x250"),
.init(location: "article", format: "300x250"),
])Use slotName to pair responses with the slots you requested:
let ad = ads.first { $0.slotName == slot.slotName }Add targeting at the request level (applies to all slots) or per slot. Keys are the fixed two-character codes configured in your Adhese account; values are string arrays.
var request = AdRequest(slots: [
.init(location: "homepage", format: "300x250", parameters: ["ps": ["top"]]), // slot-level
])
request.parameters = ["kw": ["cheese", "wine"], "ag": ["40"]] // request-levelThe SDK automatically adds the device type (dt) and device family (br).
The SDK does not gather consent. Use an IAB TCF compatible Consent Management Platform (CMP) and pass the result:
config.consent = .all // allow personalised targeting -> tl=all
config.consent = .none // no personalised targeting -> tl=none (default)
config.consent = .tcf(tcString) // explicit TCF string -> xt=<string>
config.consent = .tcfFromUserDefaults // read IABTCF_TCString from a CMP -> xt=<string>Ad tracking may require the user's permission under Apple's App Tracking
Transparency. If your integration uses the advertising identifier or otherwise
tracks users, request authorization with ATTrackingManager and add an
NSUserTrackingUsageDescription entry to your app's Info.plist. The SDK ships a
PrivacyInfo.xcprivacy manifest declaring its tracking domain and data use.
requestAds(_:) returns the ads or throws an AdheseError. Results and errors are
mutually exclusive; individual malformed ad entries are skipped and logged rather
than thrown.
do {
let ads = try await adhese.requestAds(request)
} catch let error as AdheseError {
switch error {
case .invalidConfiguration(let reason): print(reason)
case .network(let underlying): print(underlying)
case .httpStatus(let code): print("HTTP \(code)")
case .invalidResponse: print("Unparseable response")
case .noAdAvailable: print("Nothing booked for this slot")
}
}noAdAvailable is only reported through the managed loading path (AdView/
AdBanner with a slot); requestAds(_:) returns an empty array instead.
See Example/ for a runnable app demonstrating both SwiftUI and UIKit.
- iOS 15.0+
- Swift 5.9+ / Xcode 15+
MIT. See LICENSE.