Exposed Ads

Display host-driven exposed ad units on iOS on demand — overlay, sidebar, PIP, side-by-side, sponsor frame, full-screen, transparent, and VAST — with step-by-step Swift examples.

Exposed ads are ad units your Host App displays on demand — at any moment your app chooses. Your app controls both when the ad appears and what it contains: you call showAd(_:) or showVASTAd(_:vastURL:) and pass the creative or a VAST tag. Common triggers include a paused player, a break between plays, a halftime moment, or a button in your UI — but the timing is entirely yours.

Exposed ads vs. paused ads. Exposed ads are host-driven — your app decides when to show one and supplies the content. Paused ads are a separate, StreamLayer-delivered format: StreamLayer serves an ad automatically on an ad break, sourced from Studio or Prebid. Paused-ad delivery is tvOS-only — see Paused Ads (tvOS). For showing an exposed ad specifically when the viewer pauses on iOS, see Pause Ads on iOS.

There are two entry points, both on the StreamLayer class:

DeliveryMethodYou provide
Plain exposed adStreamLayer.showAd(_:)The ad unit and its creative/config (SLRAdType).
VAST exposed adStreamLayer.showVASTAd(_:vastURL:)A layout shape plus a VAST tag URL; the SDK parses and renders it.

Platform note: This guide covers the iOS SDK. tvOS exposes the same two methods with platform-specific ad units — see Exposed Ads (tvOS). Key iOS vs tvOS differences are called out in Platform differences below.

Prerequisites

Before displaying an exposed ad:

  1. Complete the iOS Integration Guide — install the SDK and obtain your SDK API Key.
  2. Create an event session so ad analytics have context:
StreamLayer.createSession(for: "your-event-id")
  1. Create the StreamLayer Element. createOverlay(...) wires the display delegate that presents exposed ads — showAd(_:) and showVASTAd(_:vastURL:) are no-ops until it has been called.
let overlayViewController = StreamLayer.createOverlay(
    mainContainerViewController: self,
    overlayDelegate: self,
    overlayDataSource: self
)

Availability: The .fullScreen, .transparentBackground, and .transparentBackgroundVAST ad units are iPad-only — calls are no-ops on iPhone. The promotion units (.overlay, .sidebar, .pictureInPicture, .sideBySide, .sponsorFrame) and .prebid work on both iPhone and iPad.

Quick Start

Import the SDK and show an ad unit whenever your app needs to:

import StreamLayerSDK

func showExposedAd() {
    let config = SLRSidebarPromotionAdConfiguration(
        media: SLRAdMedia(image: .url(URL(string: "https://cdn.example.com/ad.jpg")!)),
        title: "Special offer",
        body: "Today only — 30% off",
        cta: SLRAdCTA(url: URL(string: "https://example.com/offer")!, title: "Shop now")
    )
    StreamLayer.showAd(.sidebar(config))
}

The ad appears in the StreamLayer Element with a resume button. When the viewer dismisses it, the SDK calls streamLayerDelegateAdDidFinish() on your SLROverlayDelegate so you can resume playback. It also calls the optional streamLayerDelegateAdDidStart() when an ad begins — both callbacks are parameterless on iOS and fire for every ad the SDK presents.

Choosing when to show an ad

Because exposed ads are on demand, your app decides the trigger. Call showAd(_:) / showVASTAd(_:vastURL:) from wherever fits your product — a paused player, an inning change, a manual button. Dismiss with hideAd(), and resume your video when the SDK reports the ad finished:

// Show an ad on some app event
func onHalftime() {
    showExposedAd()
}

// Dismiss it programmatically when you no longer want it on screen
func onPlayResumed() {
    StreamLayer.hideAd()
}
extension YourViewController: SLROverlayDelegate {
    // Optional — a StreamLayer ad began presenting
    func streamLayerDelegateAdDidStart() {}

    // The ad was dismissed (resume button or auto-close) — resume playback
    func streamLayerDelegateAdDidFinish() {
        player.play()
    }
}

Showing an ad specifically when the viewer pauses is a common case with a small delay to filter accidental pauses. See the Pause Ads on iOS recipe for that pattern.


Plain exposed ads — showAd(_:)

Pass an SLRAdType case describing the ad unit. Every unit is presented over or beside the video and dismisses on resume.

public class func showAd(_ adType: SLRAdType)

Overlay ad unit

.overlay renders the ad on top of the video stream. iOS only.

let config = SLROverlayPromotionAdConfiguration(
    media: SLRAdMedia(image: .url(URL(string: "https://cdn.example.com/ad.jpg")!)),
    title: "Match sponsor",
    body: "Presented by Acme",
    cta: SLRAdCTA(url: URL(string: "https://example.com")!, title: "Learn more"),
    timing: SLRAdTiming(duration: 15),
    sponsor: SLRAdSponsor(logo: .url(URL(string: "https://cdn.example.com/logo.png")!), name: "Acme")
)
StreamLayer.showAd(.overlay(config))

Sidebar ad unit

.sidebar squeezes the video and shows the ad in a side panel. Supplying a banner promotes the layout to an L-bar (side panel plus a bottom banner).

// Sidebar
let sidebar = SLRSidebarPromotionAdConfiguration(
    media: SLRAdMedia(image: .url(URL(string: "https://cdn.example.com/ad.jpg")!)),
    title: "Special offer",
    body: "Today only — 30% off",
    cta: SLRAdCTA(url: URL(string: "https://example.com/offer")!, title: "Shop now")
)
StreamLayer.showAd(.sidebar(sidebar))

// L-bar — add a bottom banner
let lbar = SLRSidebarPromotionAdConfiguration(
    media: SLRAdMedia(image: .url(URL(string: "https://cdn.example.com/ad.jpg")!)),
    title: "Special offer",
    cta: SLRAdCTA(url: URL(string: "https://example.com/offer")!, title: "Shop now"),
    banner: SLRAdBanner(
        image: .url(URL(string: "https://cdn.example.com/banner.jpg")!),
        tapURL: URL(string: "https://example.com")
    )
)
StreamLayer.showAd(.sidebar(lbar))

Picture-in-Picture ad unit

.pictureInPicture shrinks the video into a floating window and shows the ad — sized larger than the video — behind it. Supply a video in the media for an autoplaying clip.

let config = SLRPictureInPicturePromotionAdConfiguration(
    media: SLRAdMedia(
        image: .url(URL(string: "https://cdn.example.com/poster.jpg")!),
        video: SLRAdVideo(url: URL(string: "https://cdn.example.com/promo.mp4")!)
    ),
    title: "Now streaming",
    cta: SLRAdCTA(url: URL(string: "https://example.com")!, title: "Watch"),
    sponsorLogo: .url(URL(string: "https://cdn.example.com/logo.png")!)
)
StreamLayer.showAd(.pictureInPicture(config))

Side-by-Side ad unit

.sideBySide places the ad next to the video at the same size. It takes the same configuration as Picture-in-Picture.

let config = SLRSideBySidePromotionAdConfiguration(
    media: SLRAdMedia(
        image: .url(URL(string: "https://cdn.example.com/poster.jpg")!),
        video: SLRAdVideo(url: URL(string: "https://cdn.example.com/promo.mp4")!)
    ),
    title: "Now streaming",
    cta: SLRAdCTA(url: URL(string: "https://example.com")!, title: "Watch"),
    sponsorLogo: .url(URL(string: "https://cdn.example.com/logo.png")!)
)
StreamLayer.showAd(.sideBySide(config))

Sponsor Frame ad unit

.sponsorFrame is a squeeze-back frame: the video shrinks and a background image, title, and sponsor logo fill the surrounding frame. sideAlignment places the content on the .left or .right.

let config = SLRSponsorFrameAdConfiguration(
    title: "Brought to you by Acme",
    sponsorLogo: .url(URL(string: "https://cdn.example.com/logo.png")!),
    cta: SLRAdCTA(url: URL(string: "https://example.com")!, title: "Learn more"),
    background: SLRAdBackground(.url(URL(string: "https://cdn.example.com/frame_bg.jpg")!)),
    sideAlignment: .right
)
StreamLayer.showAd(.sponsorFrame(config))

Full-Screen ad unit (iPad only)

.fullScreen fills the screen with a static creative. layout controls composition: .left (title/body left-aligned), .center (centered), or .image (background image only, no text).

let config = SLRFullScreenAdConfiguration(
    title: "Sponsored Content",
    body: "Check out our latest offers",
    sponsorLogo: UIImage(named: "sponsor_logo"),
    backgroundImage: UIImage(named: "ad_background"),
    layout: .left
)
StreamLayer.showAd(.fullScreen(configuration: config))

Transparent Background ad unit (iPad only)

.transparentBackground renders a creative in a transparent WKWebView layered over the player. Provide a creative url; optionally pass a slot identifier, and the SDK's ad proxy builds the request from the slot instead of loading the raw URL. Customize the resume button and touch pass-through — see Resume button and bypassTouches.

// Direct creative URL
StreamLayer.showAd(.transparentBackground(
    url: URL(string: "https://example.com/pause-ad.html")!,
    resumeButton: .hidden
))

// Resolve through the ad proxy with an ad-server slot
StreamLayer.showAd(.transparentBackground(
    url: URL(string: "https://example.com/pause-ad.html")!,
    slot: "/12345/pause_ad_ctv",
    resumeButton: .hidden
))

url is required. When you also pass slot, the SDK's ad proxy builds the request from the slot and handles ad-server resolution and analytics.

Transparent Background VAST ad unit (iPad only)

.transparentBackgroundVAST parses a VAST tag, renders its NonLinear / Companion static image as a transparent overlay, fires the <Impression> pixels on display, and fires <NonLinearClickTracking> on tap.

StreamLayer.showAd(.transparentBackgroundVAST(
    vastTagURL: URL(string: "https://example.com/vast.xml")!,
    resumeButton: .hidden
))

If the VAST response has no usable static image, the SDK logs an error and skips presentation — playback is not interrupted. To render VAST inside a promotion shape (overlay, sidebar, PIP, side-by-side, sponsor frame) instead, use showVASTAd(_:vastURL:).


VAST exposed ads

When your ad server returns a VAST tag, showVASTAd(_:vastURL:) renders it inside a promotion shape. The SDK fetches, parses, and displays the creative and handles VAST tracking for you.

public class func showVASTAd(_ shape: SLRExposedVASTShape, vastURL: URL)

SLRExposedVASTShape selects the layout. The overlay, sidebar, pictureInPicture, and sideBySide shapes accept an SLRExposedVASTConfiguration that can carry a host background:

let tag = URL(string: "https://example.com/vast.xml")!

// Sidebar with a host-provided background
StreamLayer.showVASTAd(
    .sidebar(SLRExposedVASTConfiguration(
        background: SLRAdBackground(.url(URL(string: "https://cdn.example.com/bg.jpg")!))
    )),
    vastURL: tag
)

// Overlay (iOS only), default configuration
StreamLayer.showVASTAd(.overlay(), vastURL: tag)

// Picture-in-Picture
StreamLayer.showVASTAd(.pictureInPicture(), vastURL: tag)

// Side-by-Side
StreamLayer.showVASTAd(.sideBySide(), vastURL: tag)

The sponsorFrame shape takes no configuration — the VAST creative fills the frame, so a host background cannot be supplied. Choose the content side with sideAlignment:

StreamLayer.showVASTAd(.sponsorFrame(sideAlignment: .right), vastURL: tag)
VAST shapeConfigurable backgroundNotes
.overlayiOS only
.sidebarAdd nothing else for a plain sidebar
.pictureInPicture
.sideBySide
.sponsorFrame(sideAlignment:)VAST creative fills the frame

Prebid ad units

Prebid exposed ads run a header-bidding auction and render the winning VAST creative. On iOS, request one on demand through showAd(.prebid(_:)) with an SLRPrebidPlacement:

StreamLayer.showAd(.prebid(
    SLRPrebidPlacement(
        storedRequestId: "sl-pause-ios",
        demandSignals: {
            SLRPrebidDemandSignals(
                advertisingIdentifier: ASIdentifierManager.shared().advertisingIdentifier.uuidString,
                isLimitAdTrackingEnabled: false
            )
        }
    )
))
  • storedRequestId — the stored request configured for your placement in the auction backend.
  • demandSignals — a closure returning per-request signals (advertising identifier, limit-ad-tracking flag). Return SLRPrebidDemandSignals.limited when the user has not granted tracking authorization.
  • cannedResponse — an optional SLRPrebidCannedResponse for testing against a stored auction response instead of a live auction.
// Testing against a stored auction response
StreamLayer.showAd(.prebid(
    SLRPrebidPlacement(
        storedRequestId: "sl-pause-ios",
        cannedResponse: SLRPrebidCannedResponse(
            storedAuctionResponseId: "mock_video_1",
            bidderPlacements: ["appnexus": 13144370]
        )
    )
))

Request tracking authorization (ATTrackingManager.requestTrackingAuthorization) before building live demand signals so the advertising identifier is available. showAd(.prebid) runs a fresh auction each call. On tvOS, Prebid can additionally be set as the automatic paused-ad delivery source — see Paused Ads (tvOS).


Prefetching and lifecycle

Prefetch

Prefetch caches an ad's VAST content and images so it appears instantly. Use prefetchAd(_:expirationInterval:completion:) for SLRAdType units and prefetchVASTAd(vastURL:expirationInterval:completion:) for VAST tags. Both are iPad-only and default to a one-hour expiration.

let vastURL = URL(string: "https://example.com/vast.xml")!

// Prefetch a VAST tag with the default 1-hour expiration
StreamLayer.prefetchVASTAd(vastURL: vastURL) { result in
    switch result {
    case .success:
        print("Ad cached for 1 hour")
    case .failure(let error):
        print("Prefetch failed: \(error)")
    }
}

// Prefetch a transparent VAST ad unit with a custom 30-minute expiration
StreamLayer.prefetchAd(
    .transparentBackgroundVAST(vastTagURL: vastURL),
    expirationInterval: 1800
) { _ in }

A prefetched ad's cache is cleared automatically once it has been presented, so the next request fetches fresh content.

Clear the cache

StreamLayer.clearPrefetchedAds()

Dismiss programmatically

hideAd() dismisses the currently displayed exposed ad. It is a no-op when no ad is visible and, on iOS, is iPad-only.

StreamLayer.hideAd()

Resume playback

The SDK dismisses the ad when the viewer taps the resume button and calls streamLayerDelegateAdDidFinish() on your SLROverlayDelegate. Resume playback there. It also calls the optional streamLayerDelegateAdDidStart() when an ad begins presenting; both are parameterless on iOS and fire for every ad.

extension YourViewController: SLROverlayDelegate {
    // Optional — a StreamLayer ad began presenting
    func streamLayerDelegateAdDidStart() {}

    // The ad was dismissed (resume button or auto-close) — resume playback
    func streamLayerDelegateAdDidFinish() {
        player.play()
    }
}

Resume button

For .transparentBackground and .transparentBackgroundVAST, customize the resume button with SLRResumeButtonConfiguration — content (image, text, or hidden) and position.

let vastURL = URL(string: "https://example.com/vast.xml")!

// Text button at the default position
StreamLayer.showAd(.transparentBackgroundVAST(
    vastTagURL: vastURL,
    resumeButton: .text("RESUME")
))

// Image button in a specific corner
let config = SLRResumeButtonConfiguration(
    content: .text("Continue"),
    position: .bottomRight(insets: UIEdgeInsets(top: 0, left: 0, bottom: 80, right: 80))
)
StreamLayer.showAd(.transparentBackgroundVAST(vastTagURL: vastURL, resumeButton: config))

// Hidden button — tap anywhere to resume
StreamLayer.showAd(.transparentBackgroundVAST(vastTagURL: vastURL, resumeButton: .hidden))

Available positions are .topLeft, .topRight, .bottomLeft, .bottomRight, .center, and .custom (a closure for full layout control). When you omit the position, the SDK applies .defaultLandscape (60pt left, 64pt from bottom) or .defaultPortrait (16pt left, 16pt from bottom) based on orientation.

bypassTouches

Both transparent units accept bypassTouches: Bool:

  • false (default) — the overlay absorbs all touches.
  • true — taps that miss the ad surface pass through to the host UI underneath (for example, player controls). Taps on the ad and resume button still work.
StreamLayer.showAd(.transparentBackgroundVAST(
    vastTagURL: vastURL,
    resumeButton: .hidden,
    bypassTouches: true
))

Configuration reference

Shared types used across the ad units:

TypePurpose
SLRAdImage.image(UIImage) or .url(URL) — an image source.
SLRAdBackgroundPortrait + landscape background pair. SLRAdBackground(_ image:) uses one image for both orientations.
SLRAdMediaThe primary image plus an optional autoplaying video (SLRAdVideo).
SLRAdVideourl, plus autoplay (default true), muted (default true), loop (default false).
SLRAdCTATappable button: url, title, optional titleColor / backgroundColor.
SLRAdSponsorlogo, name, and layout type (.left, .center, .minimal).
SLRAdTimingduration in seconds and autoclose (default true).
SLRAdBannerBottom banner for the L-bar layout: image and optional tapURL.
SLRResumeButtonConfigurationResume-button content and position for transparent units.

Platform differences

AspectiOStvOS
Overlay ad unit.overlay availableNot available
Full-screen unit.fullScreen, iPad-only.fullBleed, all devices
Transparent VASTSeparate .transparentBackgroundVAST case.transparentBackground(url:) accepts a VAST tag directly
VAST full-bleed shapeNot available.fullBleed shape available
Call-to-actionTappable button (SLRAdCTA colors)Rendered as a scannable QR code
Background typeSLRAdBackground (portrait + landscape)SLRAdImage (single image)
AvailabilityFull-screen/transparent units iPad-onlyAll units on all devices
Automatic paused-ad deliveryNot availablesetPausedAdDelivery — see Paused Ads (tvOS)

See Exposed Ads (tvOS) for the tvOS-specific units.


Migrating from showPausedAd

The exposed-ads API replaces the earlier showPausedAd "paused ad" API. The old symbols are deprecated but still compile — migrate at your convenience.

DeprecatedReplacement
showPausedAd(_:)showAd(_:)
prefetchPausedAd(_:...)prefetchAd(_:...) / prefetchVASTAd(vastURL:...)
hidePausedAd()hideAd()
clearPrefetchedPausedAds()clearPrefetchedAds()
SLRPausedAdTypeSLRAdType
SLRPausedAdConfigurationSLRFullScreenAdConfiguration

Related