Skip to content

adswag/avi

Repository files navigation

Adhese iOS SDK

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, Sendable value 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.

Installation

Swift Package Manager

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.

CocoaPods

pod 'Adhese', '~> 2.0'

Quick start

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)")
}

Configuration

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.

Rendering ads

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 the Ad data before rendering.

SwiftUI

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.

UIKit

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(_:).

Lazy loading and refresh

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:).

Delegate events

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 yourself

Custom click handling

By 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
}

Creative sandboxing

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.

Slots

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 }

Targeting parameters

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-level

The SDK automatically adds the device type (dt) and device family (br).

Consent

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>

App Tracking Transparency

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.

Error handling

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.

Example app

See Example/ for a runnable app demonstrating both SwiftUI and UIKit.

Requirements

  • iOS 15.0+
  • Swift 5.9+ / Xcode 15+

License

MIT. See LICENSE.

About

The official Adhese SDK for iOS. Swift, async/await, UIKit + SwiftUI.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors