Exposed Ads

Display host-driven exposed ad units on iOS on demand — overlay, sidebar, image-only, 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, .imageOnly, .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))

Image-Only ad unit

.imageOnly renders the same sidebar / L-bar surface, but the advertiser's creative is the unit: background is painted full-bleed behind the squeezed video, and nothing is composed over it but the call to action — which keeps the exact position it occupies in the standard sidebar layout. Because of that, this configuration takes no media, title, body, or sponsor.

background is required; an Image-Only ad has nothing else to render. Supplying a banner promotes the layout to an L-bar, with the 6:1 banner in the bottom slot.

// Image-Only sidebar
let imageOnly = SLRImageOnlyPromotionAdConfiguration(
    background: SLRAdBackground(
        portrait: .url(URL(string: "https://cdn.example.com/creative-9x16.jpg")!),
        landscape: .url(URL(string: "https://cdn.example.com/creative-16x9.jpg")!)
    ),
    cta: SLRAdCTA(url: URL(string: "https://example.com/offer")!, title: "Shop now")
)
StreamLayer.showAd(.imageOnly(imageOnly))

// Image-Only L-bar — add the 6:1 bottom banner
let imageOnlyLBar = SLRImageOnlyPromotionAdConfiguration(
    background: SLRAdBackground(.url(URL(string: "https://cdn.example.com/creative-16x9.jpg")!)),
    cta: SLRAdCTA(url: URL(string: "https://example.com/offer")!, title: "Shop now"),
    banner: SLRAdBanner(
        image: .url(URL(string: "https://cdn.example.com/banner-6x1.jpg")!),
        tapURL: URL(string: "https://example.com")
    )
)
StreamLayer.showAd(.imageOnly(imageOnlyLBar))

Supply per-orientation artwork, or pass a single image with SLRAdBackground(_:) to reuse it for both. The sidebar / L-bar surface itself is landscape-only: in portrait the ad presents as a full-bleed overlay painted with background.portrait, keeping the same call to action. Design each creative leaving the call-to-action area clear. timing behaves exactly as it does on .sidebar.

To serve this unit from an ad server instead of supplying the creative yourself, use the .imageOnly VAST shape — the tag's still creative becomes the backdrop, and a spare wide companion promotes it to L-bar.

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 imageOnly and sponsorFrame shapes take no configuration — the VAST creative fills the frame, so a host background cannot be supplied. For sponsor frame, choose the content side with sideAlignment:

StreamLayer.showVASTAd(.imageOnly, vastURL: tag)
StreamLayer.showVASTAd(.sponsorFrame(sideAlignment: .right), vastURL: tag)

.imageOnly is the VAST twin of .imageOnly(_:): the tag's still creative becomes the full-bleed backdrop, with only the call to action over it. It renders on the same sidebar / L-bar surface, and — like .sidebar — upgrades to L-bar on its own when the tag carries a spare wide companion, which fills the bottom slot. In portrait it falls back to the full-bleed overlay container, painted with the same creative. Because the surface paints a still image, a tag whose creative is video-only renders nothing.

VAST shapeConfigurable backgroundNotes
.overlayiOS only
.sidebarAdd nothing else for a plain sidebar
.imageOnlyVAST creative fills the frame; still images only
.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
VAST Image-Only shape.imageOnly.imageOnly(isPaused:) (takes a host paused intent)
Call-to-actionTappable button (SLRAdCTA colors)Rendered as a scannable QR code
Background typeSLRAdBackground (portrait + landscape)SLRAdImage (single image)
Image-Only unit.imageOnly, overlay fallback in portrait.imageOnlyPromotion, landscape only
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