> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kontext.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Guides

> Recipes beyond the first ad: loading several ads at once, your own auction, no-fill, the playhead, the next episode, the viewer, SwiftUI.

## Loading several ads at once

To have several formats ready at once, ask for them and load them concurrently. Each format is requested and rendered in parallel, and one format without an ad leaves the others untouched:

```swift theme={null}
func prepare(_ format: AdFormat) async -> VideoAd? {
    do {
        let ad = try await ads.format(format)
        try await ad.load()
        ad.delegate = self
        return ad
    } catch {
        return nil                     // no fill for this format this time
    }
}

async let overlay = prepare(.overlay)
async let pause = prepare(.pause)
async let linear = prepare(.linear)
(overlayAd, pauseAd, linearAd) = await (overlay, pause, linear)
```

## Running your own auction

`VideoAd` carries the price before anything is rendered, so you can compare it with other networks and decide:

```swift theme={null}
let ad = try await ads.format(.pause)
if ad.price.value >= otherNetworkCPM {
    try await ad.load()
    pauseAd = ad
} else {
    try ad.reject(.outbid(winningPrice: otherNetworkCPM, winner: "Other network"))
}
```

`price.value` is a CPM in `price.currency`. `price.precision` says whether it is a DSP's clearing price (`.exact`) or a forecast for house demand (`.estimated`).

`reject` tells the server at once, and the server tells the advertiser its win produced no impression. Use `.outbid` when another network won on price, with the winning price and the network, so we can optimize our bids for your traffic; `.notUsed` (the default) for any other reason: the moment passed, the viewer left. Rejecting is optional; an ad you never use is settled when it expires or when you call `destroy()`.

## Handling no-fill

`format(_:)` throws `VideoAdError.noFill(reason:)` when the server has nothing for the format. It is not a failure and the SDK never retries by itself; ask again at a later moment.

```swift theme={null}
do {
    pauseAd = try await ads.format(.pause)
} catch VideoAdError.noFill(let reason) {
    switch reason {
    case .noAdMatch: break             // no campaign fits this episode or viewer right now
    case .geoRestricted: break         // no ads served in the viewer's country
    default: break                     // frequencyCap, contentUnsafe, temporaryError, …
    }
}
```

All of them mean the same for your code: no ad this time, ask again at the next moment. Log the reason if you want to see why fill is low.

## Giving the SDK your player

Every request and event carries where the viewer is in the episode, and an overlay follows the playback: it shows over playing content only and goes away when the viewer pauses. Give the SDK your player once the episode starts playing.

With an `AVPlayer`, hand it over; the SDK reads its time and watches its playback:

```swift theme={null}
ads.track(player)
```

With any other player, conform it to `VideoAdsPlayer`: its position in seconds, and whether it plays. Buffering and seeking count as playing.

```swift theme={null}
extension MyPlayer: VideoAdsPlayer {
    var currentTime: TimeInterval { position }
    var isPlaying: Bool { state == .playing || state == .buffering }
}

ads.track(myPlayer)
```

If your player already publishes its state, return it as `isPlayingPublisher` and an overlay ends the moment the viewer pauses; otherwise the SDK checks `isPlaying` four times a second while an overlay is on screen. The SDK holds the player weakly and lets go of it at `destroy()`.

Call `track(_:)` when this episode starts playing, not when you create the `VideoAds`: until then the player's time belongs to the previous episode. Before that the SDK sends 0, which is right for a prefetch.

## Keeping the ad clear of your controls

An overlay strip sits at the bottom or top of the player view, a pause card at the top; the SDK keeps both inside the view's safe area. If your player draws its own controls there (a title, a progress bar, buttons), pass the space they take as `insets`:

```swift theme={null}
// Your control bar is 80 points tall, at the bottom of the player.
try? overlayAd?.show(in: playerView, insets: UIEdgeInsets(top: 0, left: 0, bottom: 80, right: 0))
```

* The insets add to the safe area, as UIKit's `additionalSafeAreaInsets` do: measure from the safe area's edge, not from the screen's.
* They stay while the ad is shown, through resizes and rotations. If your controls change size while an ad is on screen, the ad keeps the insets it was shown with.
* The ad still has to fit at half its size or more. Insets count against the space, so `containerTooSmall(minimum:)` includes them.
* A full-screen pause ad ignores them: it fills the view, and its own controls keep clear of the edges.

## The next episode

Create the next episode's `VideoAds` while the current one plays, prefetch its ads, and destroy the current one when the viewer leaves. Any number can be alive at once; the SDK keeps at most one ad on screen in the whole app.

```swift theme={null}
let next = VideoAds(content: nextEpisode)
Task { nextLinear = try? await next.format(.linear); try? await nextLinear?.load() }

// When the viewer moves on:
ads.destroy()
ads = next
ads.track(player)     // the player now plays the next episode
```

Call `track(_:)` when the episode actually starts in the player, not at creation: before that the player's time belongs to the previous episode.

## Who the viewer is

Set what you know on the shared instance; the next request carries it:

```swift theme={null}
KontextVideo.shared.user = .init(id: "user-1234", keywords: ["romance", "drama"])
KontextVideo.shared.variantId = "B"     // for A/B tests agreed with Kontext
```

> [!NOTE]
>
> Consent is read for you: on every request the SDK reads the IAB keys your consent platform writes to `UserDefaults`. Only when the SDK cannot detect a signal, because your app has no such platform or keeps a signal elsewhere, set it yourself. A value your consent platform wrote still wins.
>
> ```swift theme={null}
> KontextVideo.shared.regulatory = .init(
>     gdpr: true,
>     gdprConsent: "<TCF string>",
>     usPrivacy: "1YNN",
>     coppa: false
> )
> ```
>
> See [TCF & CMP](https://docs.kontext.so/video/guides/tcf-cmp.md).

## Using it from SwiftUI

The SDK presents ads with UIKit, which SwiftUI apps use as well. A linear ad needs nothing extra: `show()` presents it over whatever SwiftUI is showing. An overlay or pause ad goes into a `UIView`, which `show(in:)` takes and SwiftUI's `VideoPlayer` does not expose. Give it a transparent view laid over the player:

```swift theme={null}
/// Where overlay and pause ads appear. Taps outside the ad go through to the player.
final class AdAreaView: UIView {
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        let hit = super.hitTest(point, with: event)
        return hit === self ? nil : hit
    }
}

struct AdArea: UIViewRepresentable {
    let view: AdAreaView
    func makeUIView(context: Context) -> AdAreaView { view }
    func updateUIView(_ uiView: AdAreaView, context: Context) {}
}

@MainActor
final class PlayerModel: ObservableObject, VideoAdDelegate {
    let adArea = AdAreaView()          // created once, kept for the screen's lifetime
    // player, ads, pauseAd, … as in Showing your first ad
}

struct PlayerScreen: View {
    @StateObject private var model = PlayerModel()

    var body: some View {
        VideoPlayer(player: model.player)
            .overlay { AdArea(view: model.adArea) }
    }
}

// In PlayerModel, at the moment:
try? pauseAd?.show(in: adArea)
```

To keep the ad above a title or controls you draw over the player, pass their height as `insets`, as the [example app](https://docs.kontext.so/video/sdk/swift/example.md) does.

* Create the view once and keep it on your model, not inside `makeUIView`: SwiftUI may rebuild the representable, and the ad would go with it.
* Keep the `VideoAds` on your model too (held by `@StateObject`), not in the view struct: SwiftUI recreates view structs, and releasing the `VideoAds` ends its ads.
* `hitTest` passes taps outside the ad through, so your own gestures on the player keep working.
* The delegate is held weakly: a `@StateObject` model keeps it alive for the screen.
* A linear ad covers your view, and SwiftUI calls `onDisappear` for that. Call `ads.destroy()` when the viewer actually leaves (your back button, the next episode), not in `onDisappear`.

The [example app](https://docs.kontext.so/video/sdk/swift/example.md) is a complete SwiftUI integration of all three formats.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.