Skip to main content

Getting Started — SwiftUI

info

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

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 SDK
  • BBNativePlayerKit-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.

important

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:

PropertyTypeDescription
autoPlayBool?Auto-start playback
mutedBool?Start muted
noChromeCastBool?Disable Chromecast
noStatsBool?Disable analytics
commercialsBool?Enable/disable ads
forceFullscreenLandscapeBool?Force landscape in fullscreen
allowCollapseExpandBool?Allow collapse/expand behavior
showChromeCastMiniControlsInPlayerBool?Show Chromecast mini controls
showDescriptionBool?Show clip description
waitForCmpBool?Wait for CMP consent
handleConsentManagementBool?Handle consent management
tagForUnderAgeOfConsentBool?Tag for under-age consent
consentStringString?TCF consent string
consentGdprAppliesInt?GDPR applies (0 or 1)
consentCmpVersionInt?CMP version
adsystemPpidString?Ad system Publisher-Provided ID
adsystemBuidString?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:

PropertyTypeDescription
isPlayingBoolWhether the player is currently playing
phasePhase?Current phase: INIT, PRE, MAIN, POST, EXIT
playerStatePlayerState?Current state: IDLE, LOADING, PAUSED, PLAYING, ERROR (aliased as PlayerState to avoid conflict with SwiftUI's State)
durationDoubleContent duration in seconds
currentTimeDoublePlayback 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.
volumeDoubleCurrent volume (0.0 to 1.0)
isMutedBoolWhether 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.
isFullscreenBoolWhether the player is in fullscreen mode
isCollapsedBoolWhether the player is currently collapsed (outstream collapse / expand)
modeString?Current player mode
clipDataMediaClip?Loaded clip metadata (title, description, thumbnails, etc.)
projectDataProject?Loaded project metadata
isReadyBoolWhether the player has finished setup
jsonUrlString?The JSON URL used to initialize the player
errorString?Error message if setup or playback failed
isAdPlayingBoolWhether 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:

MethodDescription
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
]
)
}
note

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:

  1. Create the renderer with an /r/ (renderer) JSON URL.
  2. 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 UIViewController has a non-zero frame and window).
  • When using the convenience initializer (without an external state parameter), the player is destroyed automatically on .onDisappear.
  • When using an external BBPlayerState, call state.destroy() from .onDisappear to release SDK resources.
important

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 SDKSwiftUI 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 dictionaryBBPlayerOptions(...) struct
bbNativePlayerView(didTriggerPlaying:) delegateplayerState.isPlaying (reactive)
playerView.removeFromSuperview()Automatic on view removal
Manual AutoLayout constraints.aspectRatio(16/9, contentMode: .fit)
UIViewController required at creationResolved automatically from view hierarchy