Shorts (SwiftUI)
This guide covers how to integrate Blue Billywig Shorts into your SwiftUI application using the BBNativePlayerKit-SwiftUI module.
Overview
Shorts are a vertical video player experience with swipe navigation, similar to TikTok, Instagram Reels, or YouTube Shorts. They provide an engaging, mobile-first way to present short-form video content.
Shorts use a dedicated BBNativeShorts view, which is separate from the standard BBNativePlayer. This is because Shorts require a specialized native view that supports vertical swipe navigation and the full Shorts playback experience.
Key features
- Vertical video format — optimized for portrait orientation.
- Swipe navigation — users swipe up/down to navigate between clips.
- Auto-play — clips play automatically as users navigate.
- Full-screen experience — designed for immersive viewing.
- Native performance — wraps the native UIKit Shorts view from
BBNativePlayerKit. - Reactive state —
BBShortsStateuses@Observablefor per-property updates in SwiftUI.
Requirements
- iOS 17.0+
- Swift 5.9+
- Xcode 15+
- Both
BlueBillywigNativePlayerKit-iOSandBBNativePlayerKit-SwiftUIproducts frombbnativeplayerkit-cocoapod. See the SwiftUI Getting Started for installation.
1. Minimal usage (auto-cleanup)
The convenience initializer creates an internal BBShortsState and destroys it automatically on .onDisappear. This is the simplest way to drop Shorts into a screen:
import SwiftUI
import BBNativePlayerKit_SwiftUI
struct ShortsScreen: View {
let shortsId: String
var body: some View {
BBNativeShorts(
jsonUrl: "https://demo.bbvms.com/sh/\(shortsId).json"
)
}
}
Use this form whenever you don't need to observe load/error state or call destroy() from elsewhere.
2. Full control with BBShortsState
When you want to react to load completion, surface errors, or trigger cleanup manually, pass an external BBShortsState. Call state.destroy() from .onDisappear to release SDK resources:
import SwiftUI
import BBNativePlayerKit_SwiftUI
struct ShortsScreen: View {
let shortsId: String
@State private var shortsState = BBShortsState()
var body: some View {
BBNativeShorts(
jsonUrl: "https://demo.bbvms.com/sh/\(shortsId).json",
options: BBShortsOptions(displayFormat: "full"),
state: shortsState
)
.onDisappear { shortsState.destroy() }
}
}
If you pass an external state, the view does not auto-destroy it on disappear — you own the lifecycle. Forgetting shortsState.destroy() can leave audio playing in the background after the screen is dismissed.
3. Type-safe options
BBShortsOptions provides compile-time checked configuration. Use custom as an escape hatch for any options not yet covered by typed properties.
BBNativeShorts(
jsonUrl: "https://demo.bbvms.com/sh/58.json",
options: BBShortsOptions(
displayFormat: "full",
shelfStartSpacing: 16,
shelfEndSpacing: 16,
custom: [
"skipShortsAdOnSwipe": true
]
)
)
Available BBShortsOptions:
| Property | Type | Description |
|---|---|---|
displayFormat | String? | "full" (vertical swipe, default), "list" (horizontal shelf), or "player" (single player mode) |
shelfStartSpacing | Int? | Padding at the start of the shelf scroll, in points. Only meaningful with displayFormat: "list". |
shelfEndSpacing | Int? | Padding at the end of the shelf scroll, in points. Only meaningful with displayFormat: "list". |
custom | [String: Any]? | Escape hatch for additional options (typed properties take precedence). Use for keys like skipShortsAdOnSwipe. |
Display formats
| Format | Description |
|---|---|
"full" | Full-screen vertical swipe experience (default). |
"list" | Horizontal shelf / carousel — compact, scrollable thumbnails. Tapping a thumbnail opens the full-screen Shorts player as a modal. |
"player" | Single player mode. |
4. Shelf mode ("list" format)
Shelf mode displays Shorts as a horizontal carousel, ideal for embedding within a feed alongside other content. Give the shelf a fixed height rather than letting it expand:
struct ShortsShelfRow: View {
let shortsId: String
@State private var shortsState = BBShortsState()
var body: some View {
BBNativeShorts(
jsonUrl: "https://demo.bbvms.com/sh/\(shortsId).json",
options: BBShortsOptions(
displayFormat: "list",
shelfStartSpacing: 16,
shelfEndSpacing: 16
),
state: shortsState
)
.frame(height: 400)
.clipShape(RoundedRectangle(cornerRadius: 16))
.onDisappear { shortsState.destroy() }
}
}
Key differences from full mode:
- Horizontal scroll instead of vertical swipe.
- Compact view — constrain with
.frame(height:)instead of letting it fill the screen. shelfStartSpacing/shelfEndSpacingcontrol padding at the edges of the scroll.- Tapping a thumbnail opens the full-screen Shorts player as a modal overlay.
5. Observe state reactively
BBShortsState uses the @Observable macro, which provides per-property tracking. Views that read these properties update automatically when the SDK reports new values.
struct ShortsScreen: View {
let shortsId: String
@State private var shortsState = BBShortsState()
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
BBNativeShorts(
jsonUrl: "https://demo.bbvms.com/sh/\(shortsId).json",
state: shortsState
)
VStack {
Spacer()
if let error = shortsState.error {
Text(error)
.foregroundColor(.red)
.padding(.bottom, 20)
} else if !shortsState.isReady {
Text("Loading Shorts…")
.foregroundColor(.white)
.padding(.bottom, 20)
}
}
}
.onDisappear { shortsState.destroy() }
}
}
Available state properties:
| Property | Type | Description |
|---|---|---|
isReady | Bool | Becomes true when the Shorts JSON has been loaded and the view is set up. |
error | String? | Error message if setup failed. Reset to nil on destroy(). |
Method:
| Method | Description |
|---|---|
destroy() | Tear down the underlying BBNativeShortsView and release SDK resources. Safe to call multiple times. |
6. Event hooks with .onChange
For one-off side effects on state changes, use .onChange modifiers. These bridge the bbNativeShortsView(_:didSetupWithJsonUrl:) and bbNativeShortsView(_:didFailWithError:) delegate methods on BBNativeShortsViewDelegate from the underlying UIKit SDK:
BBNativeShorts(
jsonUrl: "https://demo.bbvms.com/sh/58.json",
state: shortsState
)
.onChange(of: shortsState.isReady) { _, isReady in
if isReady { print("Shorts loaded") }
}
.onChange(of: shortsState.error) { _, error in
if let error { print("Shorts error: \(error)") }
}
7. URL format
Shorts URLs follow this pattern:
https://{domain}.bbvms.com/sh/{shortsId}.json
For example:
https://demo.bbvms.com/sh/58.jsonhttps://your-publication.bbvms.com/sh/71.json
The jsonUrl is only read when the view is first created. To switch to a different Shorts configuration, dismiss the view and present a new one, or apply .id(jsonUrl) to force recreation.
8. Complete example
A full Shorts screen with status overlay and proper cleanup:
import SwiftUI
import BBNativePlayerKit_SwiftUI
struct ShortsScreen: View {
let shortsId: String
@State private var shortsState = BBShortsState()
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
BBNativeShorts(
jsonUrl: "https://demo.bbvms.com/sh/\(shortsId).json",
options: BBShortsOptions(displayFormat: "full"),
state: shortsState
)
.ignoresSafeArea()
VStack {
Spacer()
if let error = shortsState.error {
Text(error)
.font(.footnote)
.foregroundColor(Color(red: 1.0, green: 0.42, blue: 0.42))
.padding(.bottom, 32)
} else if !shortsState.isReady {
ProgressView()
.tint(.white)
.padding(.bottom, 32)
}
}
}
.onDisappear { shortsState.destroy() }
}
}
9. BBNativeShorts vs BBNativePlayer
| Feature | BBNativeShorts | BBNativePlayer |
|---|---|---|
| Vertical swipe navigation | Yes | No |
| Full-screen Shorts experience | Yes | No |
| Shelf / carousel layout | Yes (displayFormat: "list") | No |
| Standard video playback | No | Yes |
| Programmatic playback control | Not exposed (handled by the SDK) | Yes (via BBPlayerState) |
| State holder | BBShortsState | BBPlayerState |
| Options type | BBShortsOptions | BBPlayerOptions |
Do not try to load Shorts URLs (.../sh/{id}.json) in BBNativePlayer. The regular player view does not support Shorts-specific features like swipe navigation.
10. Lifecycle and cleanup
Always release the Shorts view when leaving the screen to stop playback and free native resources. Without cleanup, audio may continue playing in the background.
There are two patterns:
Automatic (convenience initializer)
When you use init(jsonUrl:options:) without passing an external state, the view manages its own BBShortsState and destroys it on .onDisappear:
BBNativeShorts(
jsonUrl: "https://demo.bbvms.com/sh/58.json"
)
// No manual cleanup needed
Manual (external state)
When you pass your own BBShortsState — required if you want to observe isReady / error — you are responsible for cleanup:
@State private var shortsState = BBShortsState()
var body: some View {
BBNativeShorts(
jsonUrl: "https://demo.bbvms.com/sh/58.json",
state: shortsState
)
.onDisappear { shortsState.destroy() }
}
This ensures:
- Video / audio playback stops when leaving the screen.
- Native
BBNativeShortsViewresources are released. - No background audio continues after navigation.
11. Layout considerations
Shorts are designed for portrait viewing. SwiftUI layout works as expected — the view fills whatever frame you give it.
Full-screen vertical
BBNativeShorts(jsonUrl: "https://demo.bbvms.com/sh/58.json")
.ignoresSafeArea()
Within safe area
BBNativeShorts(jsonUrl: "https://demo.bbvms.com/sh/58.json")
// Default behavior — respects safe area insets
Embedded shelf
BBNativeShorts(
jsonUrl: "https://demo.bbvms.com/sh/58.json",
options: BBShortsOptions(displayFormat: "list")
)
.frame(height: 400)
.clipShape(RoundedRectangle(cornerRadius: 16))
12. API reference
BBNativeShorts
| Initializer | Description |
|---|---|
init(jsonUrl:options:) | Self-managing view with internal state that auto-destroys on disappear. Use when you don't need to observe state. |
init(jsonUrl:options:state:) | External-state view for full control. Caller owns the BBShortsState lifecycle and must call state.destroy() on disappear. |
BBShortsOptions
See Section 3 above.
BBShortsState
See Section 5 above.
13. Troubleshooting
Shorts not loading
- Verify the Shorts ID exists in your publication.
- Check the JSON URL format:
https://{domain}.bbvms.com/sh/{shortsId}.json. - Ensure your publication has Shorts enabled.
- Observe
shortsState.error— the SDK reports setup failures here.
Swipe not working
- Confirm you are using
BBNativeShorts, notBBNativePlayer. - Check that the Shorts configuration contains multiple clips.
- Verify
displayFormatis"full"(or omitted —"full"is the default).
Black screen
- Check network connectivity.
- Verify the Shorts configuration contains valid video content.
- Inspect Xcode console output for SDK error messages.
- Confirm the hosting frame is non-zero. The Shorts view defers creation until
viewDidLayoutSubviewsreports non-zero bounds; if the SwiftUI parent collapses the frame, the view never initializes.
Audio continues after leaving the screen
This is the most common cleanup bug. Pick one of:
- Switch to the convenience initializer
BBNativeShorts(jsonUrl:options:)if you don't need to observe state. - Add
.onDisappear { shortsState.destroy() }to the view that hostsBBNativeShorts.
Assertion: jsonUrl changed after creation
In DEBUG builds, the SwiftUI wrapper asserts that jsonUrl is stable for the lifetime of the view. To switch Shorts content, force view recreation by applying .id(jsonUrl) to the parent, or pop and push a new screen.