Getting Started — SwiftUI
This guide covers the optional SwiftUI module, which wraps the core UIKit SDK in a native SwiftUI View with reactive state. If your app is UIKit-based — or you target iOS 16 or earlier — use the UIKit Getting Started instead. See also the SwiftUI module overview for when to pick this over the UIKit SDK.
This guide shows you how to integrate the Blue Billywig video player into a SwiftUI application. The SwiftUI module provides a native SwiftUI View with reactive state via the @Observable macro (per-property tracking), type-safe options, and automatic lifecycle management.
Requirements:
- iOS 17.0+
- Swift 5.9+
- Xcode 15+
1. Add the dependency
Swift Package Manager (recommended)
In Xcode, go to File → Add Package Dependencies and enter the repository URL:
https://github.com/bluebillywig/bbnativeplayerkit-cocoapod
When prompted, select both library products:
BlueBillywigNativePlayerKit-iOS— the core SDKBBNativePlayerKit-SwiftUI— the SwiftUI wrapper
Alternatively, add both products in your Package.swift:
dependencies: [
.package(url: "https://github.com/bluebillywig/bbnativeplayerkit-cocoapod", exact: "<version>"),
],
targets: [
.target(
name: "MyApp",
dependencies: [
.product(name: "BlueBillywigNativePlayerKit-iOS", package: "bbnativeplayerkit-cocoapod"),
.product(name: "BBNativePlayerKit-SwiftUI", package: "bbnativeplayerkit-cocoapod"),
]
),
]
CocoaPods
Add the SwiftUI pod alongside the core pod in your Podfile:
platform :ios, '17.0'
target 'MyApp' do
pod 'BlueBillywigNativePlayerKit-iOS', '~><version>'
pod 'BlueBillywigNativePlayerKit-SwiftUI', '~><version>'
end
Then run:
pod install --repo-update
2. Add a player to your SwiftUI view
Import the SwiftUI module and add a BBNativePlayer view. Create a BBPlayerState with @State to hold the reactive player state.
import SwiftUI
import BBNativePlayerKit_SwiftUI
struct VideoScreen: View {
@State private var playerState = BBPlayerState()
var body: some View {
BBNativePlayer(
jsonUrl: "https://demo.bbvms.com/p/default/c/4256635.json",
state: playerState
)
.aspectRatio(16/9, contentMode: .fit)
}
}
That's it — the player loads and plays using playout defaults. Standard SwiftUI modifiers control sizing and layout.
The jsonUrl is only read when the view first appears. To switch content after the player is initialized, use the load methods on BBPlayerState. If you need to recreate the player with a different URL, change the view's id modifier to force recreation (e.g. .id(jsonUrl)).
3. Use type-safe options
Instead of passing an untyped [String: Any]? dictionary, use BBPlayerOptions for compile-time checked configuration:
BBNativePlayer(
jsonUrl: "https://demo.bbvms.com/p/default/c/4256635.json",
options: BBPlayerOptions(
autoPlay: false,
noChromeCast: true,
muted: true
),
state: playerState
)
.aspectRatio(16/9, contentMode: .fit)
Available options:
| Property | Type | Description |
|---|---|---|
autoPlay | Bool? | Auto-start playback |
muted | Bool? | Start muted |
noChromeCast | Bool? | Disable Chromecast |
noStats | Bool? | Disable analytics |
commercials | Bool? | Enable/disable ads |
forceFullscreenLandscape | Bool? | Force landscape in fullscreen |
allowCollapseExpand | Bool? | Allow collapse/expand behavior |
showChromeCastMiniControlsInPlayer | Bool? | Show Chromecast mini controls |
showDescription | Bool? | Show clip description |
waitForCmp | Bool? | Wait for CMP consent |
handleConsentManagement | Bool? | Handle consent management |
tagForUnderAgeOfConsent | Bool? | Tag for under-age consent |
consentString | String? | TCF consent string |
consentGdprApplies | Int? | GDPR applies (0 or 1) |
consentCmpVersion | Int? | CMP version |
adsystemPpid | String? | Ad system Publisher-Provided ID |
adsystemBuid | String? | Ad system BUID |
adTagUrlParams | [String: String]? | Custom ad tag URL parameters (keys without the adTagUrlParam_ prefix) |
custom | [String: Any]? | Escape hatch for additional options (typed properties take precedence) |
4. Observe player state reactively
BBPlayerState uses the @Observable macro, which provides per-property tracking. SwiftUI views that read these properties automatically update when values change — only the specific properties you access trigger re-renders.
struct VideoScreen: View {
@State private var playerState = BBPlayerState()
var body: some View {
VStack {
BBNativePlayer(
jsonUrl: "https://demo.bbvms.com/p/default/c/4256635.json",
state: playerState
)
.aspectRatio(16/9, contentMode: .fit)
// This text updates automatically when duration changes
Text("Duration: \(playerState.duration, specifier: "%.1f")s")
// Show phase and state
Text("Phase: \(String(describing: playerState.phase)) | State: \(String(describing: playerState.playerState))")
// Show clip title when loaded
if let title = playerState.clipData?.title {
Text("Now playing: \(title)")
}
// Show error if any
if let error = playerState.error {
Text("Error: \(error)")
.foregroundColor(.red)
}
}
}
}
Available state properties:
| Property | Type | Description |
|---|---|---|
isPlaying | Bool | Whether the player is currently playing |
phase | Phase? | Current phase: INIT, PRE, MAIN, POST, EXIT |
playerState | PlayerState? | Current state: IDLE, LOADING, PAUSED, PLAYING, ERROR (aliased as PlayerState to avoid conflict with SwiftUI's State) |
duration | Double | Content duration in seconds |
currentTime | Double | Playback position in seconds. Only updated on seek, clip load, and playback end — there is no periodic time-update callback, so building a progress bar requires your own timer. |
volume | Double | Current volume (0.0 to 1.0) |
isMuted | Bool | Whether the player is muted. Only updated when setMuted(_:) is called from Swift — the SDK does not provide a mute-change callback, so changes made via the player's own skin controls are not reflected here. |
isFullscreen | Bool | Whether the player is in fullscreen mode |
isCollapsed | Bool | Whether the player is currently collapsed (outstream collapse / expand) |
mode | String? | Current player mode |
clipData | MediaClip? | Loaded clip metadata (title, description, thumbnails, etc.) |
projectData | Project? | Loaded project metadata |
isReady | Bool | Whether the player has finished setup |
jsonUrl | String? | The JSON URL used to initialize the player |
error | String? | Error message if setup or playback failed |
isAdPlaying | Bool | Whether an ad is currently playing |
5. Control the player
Use the methods on BBPlayerState to control playback. These can be called from button actions, .onChange modifiers, or .task blocks.
struct PlayerControls: View {
var playerState: BBPlayerState
var body: some View {
HStack(spacing: 12) {
// Play / Pause toggle
Button(action: {
playerState.isPlaying ? playerState.pause() : playerState.play()
}) {
Image(systemName: playerState.isPlaying ? "pause.fill" : "play.fill")
.font(.title2)
}
// Seek to 30 seconds
Button("Skip to 0:30") {
playerState.seek(to: 30.0)
}
// Seek forward 10 seconds
Button("+10s") {
playerState.seekRelative(10.0)
}
// Mute toggle
Button(action: {
playerState.setMuted(!playerState.isMuted)
}) {
Image(systemName: playerState.isMuted ? "speaker.slash.fill" : "speaker.wave.2.fill")
}
// Fullscreen
Button(action: {
playerState.enterFullScreen()
}) {
Image(systemName: "arrow.up.left.and.arrow.down.right")
}
}
}
}
Available control methods:
| Method | Description |
|---|---|
play() | Start or resume playback |
pause() | Pause playback |
seek(to: Double) | Seek to an absolute position in seconds |
seekRelative(_ offset: Double) | Seek relative to current position (e.g. +10.0 or -10.0) |
setVolume(_ volume: Double) | Set volume (0.0 to 1.0) |
setMuted(_ muted: Bool) | Mute or unmute |
enterFullScreen() | Enter fullscreen mode |
exitFullScreen() | Exit fullscreen mode |
collapse() | Collapse the player (outstream) |
expand() | Expand the player (outstream) |
loadClip(id:autoPlay:seekTo:) | Load a different clip by ID |
loadProject(id:autoPlay:) | Load a different project by ID |
loadClipList(id:autoPlay:) | Load a clip list by ID |
autoPlayNextCancel() | Cancel queued auto-play of the next clip |
showCastPicker() | Present the Google Cast device picker (initializes Cast on first use) |
destroy() | Destroy the player and release resources (normally handled automatically) |
6. Use event callbacks (optional)
For one-off side effects on state changes, use .onChange modifiers:
BBNativePlayer(
jsonUrl: "https://demo.bbvms.com/p/default/c/4256635.json",
state: playerState
)
.aspectRatio(16/9, contentMode: .fit)
.onChange(of: playerState.isReady) { _, isReady in
if isReady { print("Player is ready") }
}
.onChange(of: playerState.error) { _, error in
if let error { print("Player error: \(error)") }
}
7. Load content dynamically
To switch content after the player has initialized, use the load methods on the state object:
struct PlaylistScreen: View {
let clipIds = ["4256635", "4256636", "4256637"]
@State private var playerState = BBPlayerState()
var body: some View {
VStack {
BBNativePlayer(
jsonUrl: "https://demo.bbvms.com/p/default.json",
state: playerState
)
.aspectRatio(16/9, contentMode: .fit)
// List of clips to load
ForEach(clipIds, id: \.self) { clipId in
Button("Play clip \(clipId)") {
playerState.loadClip(id: clipId)
}
}
}
}
}
8. Shorts (vertical video)
The SwiftUI module also provides a BBNativeShorts view for the Blue Billywig Shorts experience:
import SwiftUI
import BBNativePlayerKit_SwiftUI
struct ShortsScreen: View {
@State private var shortsState = BBShortsState()
var body: some View {
BBNativeShorts(
jsonUrl: "https://demo.bbvms.com/sh/1.json",
options: BBShortsOptions(displayFormat: "full"),
state: shortsState
)
.onDisappear { shortsState.destroy() }
}
}
For the full guide — display formats, shelf mode, lifecycle, troubleshooting — see Shorts.
9. Modal player
For a full-screen overlay experience, use BBNativePlayer.createModalPlayerView(). This presents a native modal with auto-rotation and immersive system UI.
import BBNativePlayerKit
// From a button action (find the topmost view controller)
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootVC = windowScene.windows.first?.rootViewController {
var topVC = rootVC
while let presented = topVC.presentedViewController {
topVC = presented
}
BBNativePlayer.createModalPlayerView(
uiViewContoller: topVC,
jsonUrl: "https://demo.bbvms.com/p/default/c/4256635.json",
options: [
"autoPlay": true,
"showBackArrow": true, // Shows a back arrow to dismiss
]
)
}
The modal player uses the UIKit-based SDK API directly since it presents a new view controller. The showBackArrow option displays a back arrow button in the top-left corner for dismissal.
The uiViewContoller: parameter spelling (missing the second "r") is the actual SDK parameter name, preserved for backward compatibility — it is not a typo in this guide.
10. Outstream / Ad Renderer
For in-article outstream ads, use BBNativeRenderer. The renderer requires a two-step setup:
- Create the renderer with an
/r/(renderer) JSON URL. - Once ready, call
bootstrap()with a VAST ad configuration to trigger the ad.
import SwiftUI
import BBNativePlayerKit_SwiftUI
struct OutstreamArticle: View {
@State private var rendererState = BBRendererState()
var body: some View {
ScrollView {
VStack {
Text("Article content...")
.padding()
BBNativeRenderer(
jsonUrl: "https://demo.bbvms.com/r/native_sdk_renderer.json",
state: rendererState
)
.frame(height: rendererState.isAdPlaying ? 200 : 0)
.clipped()
.animation(.spring(), value: rendererState.isAdPlaying)
.onChange(of: rendererState.isReady) { _, isReady in
if isReady {
rendererState.bootstrap(config: [
"code": "my_ad_unit",
"vastUrl": "https://example.com/vast.xml"
])
}
}
Text("More article content...")
.padding()
}
}
}
}
bootstrap(config:playoutOverrides:) accepts:
config: [String: String]— required. Must contain a"code"key and at least one of"vastUrl"or"vastXml". Optionally accepts"vastSubtype".playoutOverrides: [String: Any]?— optional overrides for playout settings.
The call is ignored (and an error logged on rendererState.error) if invoked before isReady becomes true or after destroy(). See the Renderer documentation for full details.
BBRendererState exposes isReady, error, and isAdPlaying as observable properties (tracked per-property via @Observable).
11. Complete example
Here is a complete, self-contained example of a video player screen with custom controls:
import SwiftUI
import BBNativePlayerKit_SwiftUI
struct VideoPlayerScreen: View {
@State private var playerState = BBPlayerState()
var body: some View {
VStack(spacing: 0) {
// Player
BBNativePlayer(
jsonUrl: "https://demo.bbvms.com/p/default/c/4256635.json",
options: BBPlayerOptions(
autoPlay: false,
noChromeCast: true
),
state: playerState
)
.aspectRatio(16/9, contentMode: .fit)
// Controls
HStack(spacing: 16) {
Button(action: {
playerState.isPlaying ? playerState.pause() : playerState.play()
}) {
Image(systemName: playerState.isPlaying ? "pause.fill" : "play.fill")
.font(.title2)
}
Button("+10s") {
playerState.seekRelative(10.0)
}
Button(action: {
playerState.enterFullScreen()
}) {
Image(systemName: "arrow.up.left.and.arrow.down.right")
}
Spacer()
Text("\(playerState.duration, specifier: "%.0f")s")
.font(.caption)
.foregroundColor(.secondary)
}
.padding()
// Info
VStack(alignment: .leading, spacing: 4) {
if let title = playerState.clipData?.title {
Text(title)
.font(.headline)
}
Text("Phase: \(String(describing: playerState.phase)) | State: \(String(describing: playerState.playerState))")
.font(.caption)
.foregroundColor(.secondary)
}
.padding(.horizontal)
Spacer()
}
}
}
12. Lifecycle and cleanup
The BBNativePlayer view handles lifecycle automatically:
- The player is created when the view appears in the hierarchy (deferred until the hosting
UIViewControllerhas a non-zero frame and window). - When using the convenience initializer (without an external
stateparameter), the player is destroyed automatically on.onDisappear. - When using an external
BBPlayerState, callstate.destroy()from.onDisappearto release SDK resources.
The underlying SDK requires a UIViewController for modal presentation and consent forms. The SwiftUI wrapper discovers this automatically via the responder chain. In rare cases where the view is deeply nested inside custom UIViewRepresentable containers, make sure the hosting view controller is reachable.
Migration from the UIKit SDK
If you are already using BBNativePlayer.createPlayerView() in UIKit, you can adopt SwiftUI incrementally. The SwiftUI module is a separate target — your existing UIKit code continues to work unchanged.
| UIKit SDK | SwiftUI module |
|---|---|
BBNativePlayer.createPlayerView(uiViewController:, frame:, jsonUrl:, options:) | BBNativePlayer(jsonUrl:, options:, state:) |
playerView.delegate = self | @State var state = BBPlayerState() (automatic) |
playerView.player.play() | playerState.play() |
[String: Any]? options dictionary | BBPlayerOptions(...) struct |
bbNativePlayerView(didTriggerPlaying:) delegate | playerState.isPlaying (reactive) |
playerView.removeFromSuperview() | Automatic on view removal |
| Manual AutoLayout constraints | .aspectRatio(16/9, contentMode: .fit) |
UIViewController required at creation | Resolved automatically from view hierarchy |