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:
| Delivery | Method | You provide |
|---|---|---|
| Plain exposed ad | StreamLayer.showAd(_:) | The ad unit and its creative/config (SLRAdType). |
| VAST exposed ad | StreamLayer.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:
- Complete the iOS Integration Guide — install the SDK and obtain your SDK API Key.
- Create an event session so ad analytics have context:
StreamLayer.createSession(for: "your-event-id")- Create the StreamLayer Element.
createOverlay(...)wires the display delegate that presents exposed ads —showAd(_:)andshowVASTAd(_:vastURL:)are no-ops until it has been called.
let overlayViewController = StreamLayer.createOverlay(
mainContainerViewController: self,
overlayDelegate: self,
overlayDataSource: self
)Availability: The
.fullScreen,.transparentBackground, and.transparentBackgroundVASTad units are iPad-only — calls are no-ops on iPhone. The promotion units (.overlay,.sidebar,.pictureInPicture,.sideBySide,.sponsorFrame) and.prebidwork 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(_:)
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
))
urlis required. When you also passslot, 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 shape | Configurable background | Notes |
|---|---|---|
.overlay | ✅ | iOS only |
.sidebar | ✅ | Add 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). ReturnSLRPrebidDemandSignals.limitedwhen the user has not granted tracking authorization.cannedResponse— an optionalSLRPrebidCannedResponsefor 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:
| Type | Purpose |
|---|---|
SLRAdImage | .image(UIImage) or .url(URL) — an image source. |
SLRAdBackground | Portrait + landscape background pair. SLRAdBackground(_ image:) uses one image for both orientations. |
SLRAdMedia | The primary image plus an optional autoplaying video (SLRAdVideo). |
SLRAdVideo | url, plus autoplay (default true), muted (default true), loop (default false). |
SLRAdCTA | Tappable button: url, title, optional titleColor / backgroundColor. |
SLRAdSponsor | logo, name, and layout type (.left, .center, .minimal). |
SLRAdTiming | duration in seconds and autoclose (default true). |
SLRAdBanner | Bottom banner for the L-bar layout: image and optional tapURL. |
SLRResumeButtonConfiguration | Resume-button content and position for transparent units. |
Platform differences
| Aspect | iOS | tvOS |
|---|---|---|
| Overlay ad unit | .overlay available | Not available |
| Full-screen unit | .fullScreen, iPad-only | .fullBleed, all devices |
| Transparent VAST | Separate .transparentBackgroundVAST case | .transparentBackground(url:) accepts a VAST tag directly |
| VAST full-bleed shape | Not available | .fullBleed shape available |
| Call-to-action | Tappable button (SLRAdCTA colors) | Rendered as a scannable QR code |
| Background type | SLRAdBackground (portrait + landscape) | SLRAdImage (single image) |
| Availability | Full-screen/transparent units iPad-only | All units on all devices |
| Automatic paused-ad delivery | Not available | setPausedAdDelivery — see Paused Ads (tvOS) |
See Exposed Ads (tvOS) for the tvOS-specific units.
Migrating from showPausedAd
showPausedAdThe exposed-ads API replaces the earlier showPausedAd "paused ad" API. The old symbols are deprecated but still compile — migrate at your convenience.
| Deprecated | Replacement |
|---|---|
showPausedAd(_:) | showAd(_:) |
prefetchPausedAd(_:...) | prefetchAd(_:...) / prefetchVASTAd(vastURL:...) |
hidePausedAd() | hideAd() |
clearPrefetchedPausedAds() | clearPrefetchedAds() |
SLRPausedAdType | SLRAdType |
SLRPausedAdConfiguration | SLRFullScreenAdConfiguration |
Related
- Pause Ads on iOS — Recipe: show an exposed ad when the viewer pauses
- Paused Ads (tvOS) — StreamLayer-delivered paused ads (Studio / Prebid) on ad breaks
- iOS Integration Guide — SDK installation, initialization, and StreamLayer Element setup
- iOS API Reference — Reference for
SLRAdType,SLRExposedVASTShape, and the ad configuration types - Exposed Ads (tvOS) — tvOS equivalent guide
Updated 8 days ago
