Companion ads
Companion ads display standard, carousel, or custom call-to-action creatives
alongside ads played through FLPlayer. You provide a CompanionContainerView;
the presentation manages its height and the content inside it. This guide also
covers contextual ads scheduled with playhead boundaries and takeover ads that
your app parses and renders; those flows aren't formats managed by
CompanionPresentation.
For the equivalent platform guides, see Android companion ads and Web companion ads. The APIs and some provider capabilities differ by platform.
Schedule contextual ads
Use the playhead boundary observer APIs to coordinate contextual ad rendering at
selected stream-time positions. The observer reports timing only; your app
parses the contextual ad data and renders the ad. This integration is separate
from CompanionPresentation. Register an observer for each playback with the
positions you want to observe.
let observerToken = player.addPlayheadBoundaryObserver(
positions: [30, 60],
leadTimeMs: 1_000
) { position in
// Coordinate rendering the contextual ad for this stream-time position.
}
leadTimeMs sets how long before each position the callback fires. The callback
receives the boundary position. Retain the returned token for the current
playback and remove the observer when playback completes or the user stops
playback:
if let observerToken = observerToken {
player.removePlayheadBoundaryObserver(token: observerToken)
}
Registration can return nil; handle the case where no observer token is
available.
Implement takeover ads in the application
Takeover ads are not a CompanionPresentation format. The app must interpret
the ad metadata and own takeover rendering; the SDK doesn't parse takeover
parameters or create a takeover WebView.
| Takeover type | App input | App responsibility |
|---|---|---|
| In-video | IMA ad events and the ad's traffickingParameters | Parse the takeover fields, then show and dismiss the takeover with the ad lifecycle. |
| Contextual | Playhead boundaries and contextual ad metadata | Parse the contextual ad data and render it at the intended stream-time position. |
| Ads under a tab | Details-page ad data | Parse and render the ad in the app's page UI; this doesn't use the player companion container. |
The takeover design uses different metadata for each type. These are
ad-server/application conventions, not CompanionPresentation parameters:
| Takeover type | Source | Keys or fields |
|---|---|---|
| In-video | VAST traffickingParameters | ad_type=webview, campaign, and Image_url (the page URL despite its name). |
| Contextual | GAM native custom format | ad_type=takeoverAds and Takeover_ads_url. |
| Ads under a tab, GAM | GAM native custom format | ad_type=takeoverAds, Takeover_ads_url, redirection_url, sidelogo, c_title, thirdPartyImpression, thirdPartyClick, and advertiser_name. |
| Ads under a tab, backend | Details-page backend data | advertiserTab.adType and takeoverAdsUrl in the iOS model. |
For example, the in-video VAST parameters may look like this:
ad_type=webview,campaign=<campaign-name>,Image_url=<https-url>
The design identifies ad_type=webview as an Android convention and notes
that iOS handling isn't confirmed. Verify that this format applies to your iOS
app and ad setup; it isn't a built-in iOS SDK format.
For in-video takeovers, verify that the ad model used by your integration
exposes the raw traffickingParameters. The playhead boundary observer
provides timing only; it doesn't provide or parse takeover metadata.
If the ad model doesn't expose this value, it must be made available to the app
as optional ad metadata; takeover-specific parsing still belongs in the app.
Parse parameters by key rather than relying on their order, and split each
key-value entry at its first = so URLs with query strings remain intact.
Validate destination URLs according to your app's security policy before
loading or opening them. The app also owns WebView lifecycle, click handling,
and takeover analytics.
Supported companion formats
The presentation displays one companion format at a time. It supports
SDK-rendered carousel and custom CTA creatives, plus a native IMA slot when you
configure a fixed standardSlotSize.
| Format | Creative source | Rendering | Behavior |
|---|---|---|---|
| Carousel | Ad trafficking parameters | iOS SDK | Portrait-only; can remain visible after the ad when retention is enabled. |
| Custom CTA | Ad trafficking parameters | iOS SDK | Displays an icon, title, optional description, and action button. |
| Standard native slot | IMA companion response | Google IMA in an SDK-owned view | Fixed size per presentation; unavailable on tvOS. |
Use the native slot as a fallback for ads without an eligible in-house creative
when the .standard format is enabled and a fixed size is configured. If a
required carousel or CTA image fails to load, the presentation also falls back
to the native slot when those settings are in place. Carousels switch to the
native slot in landscape when it is available.
Set up a companion presentation
Add the container to your view hierarchy, constrain its position, and set its
width through prefferedWidth. Don't add a separate width or height constraint:
the container applies its width internally, and the presentation updates its
height to fit the active creative.
import FLPlayer
let companionView = CompanionContainerView()
// Add companionView to your view hierarchy and constrain its position.
// Set this to the width available in your layout.
companionView.prefferedWidth = availableWidth
let companionSize = UIDevice.current.userInterfaceIdiom == .pad
? CGSize(width: 728, height: 90)
: CGSize(width: 320, height: 50)
let companionPresenter = CompanionPresentation(
containerView: companionView,
standardSlotSize: companionSize,
configuration: CompanionConfiguration(
formats: [.standard, .carousel, .customCTA]
),
actions: companionActions
)
companionPresenter.onReceiveEvents { event in
print("Companion event: \(event.kind)")
}
standardSlotSize is optional and sets the fixed size for the native IMA
companion slot. Use these standard banner sizes as a starting point:
| Device | Standard slot size |
|---|---|
| iPhone | 320 × 50 points |
| iPad | 728 × 90 points |
The 320 × 50 banner is a standard size for phones and tablets; 728 × 90 is a standard tablet leaderboard size. Use the iPad size only when the ad inventory supports it and the available layout can fit it. For more information, see Google's fixed-size banner ad sizes.
The slot size applies to the native IMA fallback slot, not to every creative
rendered by CompanionPresentation. Set prefferedWidth to the width available
to the container and update it when the layout changes, including after device
rotation. The presentation manages the container height. Pass nil for
standardSlotSize when you don't need a fixed native IMA slot.
Supply creative parameters
The SDK reads in-house creatives from comma-separated key=value parameters.
Use the parameter names and capitalization shown below. The parser splits each
segment at its first =, so URL query strings are preserved. A malformed
segment or duplicate key rejects the creative.
Custom CTA
A custom CTA requires an icon, title, and action URL. If the ad doesn't specify
ctaText, the SDK displays “Learn more.”
| Parameter | Required | Description |
|---|---|---|
icon or Icon | Yes | HTTP or HTTPS URL for the icon. If both aliases are present, their values must match. |
bannerTitle | Yes | Non-empty title. |
ctaUrl | Yes | Destination URL. |
bannerDescription | No | Description shown below the title. |
ctaText | No | Action button label. Defaults to Learn more. |
campaign or campaign name | No | Campaign name included in presentation events. If both aliases are present, their values must match. |
companion_schema_version | No | Schema version. The only supported value is 1. |
If you don't provide a CompanionActions handler, the SDK opens HTTP and HTTPS
destinations with UIApplication. Provide a handler to process app-specific
actions or other URL schemes.
Carousel
Set ad_type=carousel to identify a carousel. Required image and title
parameters must be valid; the description key is required but its value can be
empty.
| Parameter | Required | Description |
|---|---|---|
carousel_banner | Yes | HTTP or HTTPS URL for the collapsed banner. |
brand_icon | Yes | HTTP or HTTPS URL for the expanded header icon. |
carousel_title | Yes | Non-empty title. |
carousel_description | Yes | Description; an empty value is allowed. |
carousel_images | Yes | $-separated HTTP or HTTPS image URLs. Include at least one usable image and no more than 20 entries. |
carousel_images_urls | No | $-separated tap destinations matched to images by position. |
carousel_cta_text and carousel_cta_url | Optional pair | Shared action button label and HTTP or HTTPS destination. Provide both or neither. |
campaign or campaign name | No | Campaign name included in presentation events. If both aliases are present, their values must match. |
companion_schema_version | No | Schema version. The only supported value is 1. |
Invalid image URLs are skipped. A blank entry in carousel_images_urls leaves
the corresponding image without a tap destination.
Connect the presenter to an ad player
Pass the presenter to the ad integration that delivers the companion ads. The same presentation API supports Google IMA and MediaTailor.
Google IMA
Pass companionPresenter to FLAdvertisingGoogleIMAFactory.adPlayer. If you
prefetch ads, create a separate presenter and container for QPAdsLoader.
let prefetchPresenter = CompanionPresentation(
containerView: CompanionContainerView(),
standardSlotSize: companionSize
)
QPAdsLoader.shared.initAdsLoader(
adRequest: adRequest,
companionPresenter: prefetchPresenter
)
let adPlayer = FLAdvertisingGoogleIMAFactory.adPlayer(
contentPlayer: player,
request: adRequest,
enablePrefetchAds: true,
companionPresenter: companionPresenter,
playerViewControllerCallback: { viewController }
)
The SDK prepares and adopts prefetched slots. You don't need to call
prepareSlots(_:) or adoptSlots(_:) for this integration.
MediaTailor
Pass the presenter to the MediaTailor player builder before calling build().
var builder = FLMediatailorFactory.createPlayerBuilder(
mediatailorSession: session,
fallbackAsset: fallbackAsset
)
builder.withCompanionPresenter(companionPresenter)
let adPlayer = builder.build()
Configure creatives and handle actions
CompanionConfiguration controls which creative formats can display, whether a
carousel remains visible after an ad ends, its theme, and the image loading
timeout.
| Setting | Default | Description |
|---|---|---|
formats | .standard, .carousel, and .customCTA | Creative formats the presentation can render. |
persistCarouselAfterAd | false | Keeps a carousel visible after the ad ends. |
theme | CompanionTheme() | Sets optional surface, text, button, and button text colors. |
imageTimeout | 10 seconds | Maximum time to wait for a creative image. |
presentationMode controls when carousel creatives are eligible. It defaults
to .automatic, which uses the current orientation. Set it to .portrait,
.landscape, or .fullscreen to force a mode. Changing the mode re-evaluates
the active carousel.
Implement CompanionActions to handle creative click-through actions. Call the
completion handler exactly once with the result.
import UIKit
final class AppCompanionActions: CompanionActions {
func execute(
_ action: CompanionAction,
completion: @escaping (CompanionActionResult) -> Void
) {
UIApplication.shared.open(action.url) { success in
completion(
success
? .completed
: .failed("Could not open \(action.url)")
)
}
}
}
Register onReceiveEvents(_:) to observe presentation, expansion, asset, and
action results. The SDK delivers event callbacks on the main thread.
Events include displayDecision, capabilityLimited, cleared, retained,
expansionChanged, itemChanged, assetFailed, and action request and outcome
events.
Manage the presentation lifecycle
The ad player manages the presentation lifecycle for Google IMA and MediaTailor. If you build a custom ad integration, call the lifecycle methods in this order:
| Method | When to call |
|---|---|
prepareSlots(_:) | Before the ad request, to get native slot configuration for the IMA display container. |
adoptSlots(_:) | When adopting a native slot created for a prefetched request. |
adDidStart(_:) | When an ad starts. |
adDidEnd(_:) | When an ad ends. |
contentDidResume() | When content playback resumes after an ad break. |
dispose() | When the ad session ends. Cancels pending work and removes presentation-owned views. |
All lifecycle methods are safe to call from any thread. Calls after
dispose() are ignored. On tvOS, native IMA companion slots aren't available:
prepareSlots(_:) returns an empty array and the presentation emits a
capabilityLimited event.
After an ad ends and content resumes, the presentation clears the creative.
When persistCarouselAfterAd is true, the carousel remains visible and the
presentation emits a retained event.
Use the container in SwiftUI
SwiftUI apps can host CompanionContainerView through a
UIViewRepresentable wrapper. Implement sizeThatFits to read the
presentation-managed height; this API requires iOS 16 or tvOS 16. Set the
container width from the available layout width and update it when that width
changes.
struct CompanionContainerRepresentable: UIViewRepresentable {
let width: CGFloat
let container: CompanionContainerView
func makeUIView(context: Context) -> CompanionContainerView {
container.setContentHuggingPriority(.defaultLow, for: .horizontal)
container.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return container
}
func updateUIView(_ uiView: CompanionContainerView, context: Context) {
DispatchQueue.main.async {
uiView.prefferedWidth = width
}
}
@available(iOS 16.0, tvOS 16.0, *)
func sizeThatFits(
_ proposal: ProposedViewSize,
uiView: CompanionContainerView,
context: Context
) -> CGSize? {
let height = uiView.preferredHeight.isFinite
? max(0, uiView.preferredHeight)
: 0
return CGSize(width: width, height: height)
}
}
In your SwiftUI view, pass the available width to the wrapper. Use a stable
.id(...) based on the container identity if SwiftUI must recreate the wrapper
when the container changes.