Initial Setup for SwiftUI
This guide covers the core concepts, architectural benefits, and a step-by-step implementation to help you integrate Storyly Placement into your SwiftUI application.
What is Placement
Storyly Placement is a powerful, server-driven framework designed to dynamically render various widget experiences—such as Story Bars, Banners, and Swipe Cards—within a single host view. By decoupling the UI configuration from your app's codebase, Placement enables you to switch between different widget types in real-time without requiring a new app release.
Placement Architecture
Server Driven SurfacePlacement is a server-driven surface that can render different Storyly experiences (e.g., Story Bar, Banner) in a single host view, based on configuration and rules fetched at runtime.
Single Integration PointIt centralizes data loading, rendering, analytics, and commerce hooks via a single provider and a single SwiftUI view, enabling sophisticated, dynamic experiences without hardcoding which widget to show.
Core Building Blocks
These core building blocks are mandatory to integrate and start enabling features with Storyly Flows
Placement Blocks
Placement Data ProviderIt fetches and manages the content/config for a placement and exposes delegate callbacks. It is caller-owned and must outlive SwiftUI view updates.
Placement ConfigIt controls test mode, user/targeting context, and layout direction.
StorylyPlacement ViewIt hosts the actual widget determined at runtime (e.g., Story Bar, Banner), exposes UI and analytics callbacks, and is sized by the layout modifiers you apply to it.
Setup SDK
Add Package
Before you use Storyly Placement in your app, you must first add the Storyly Placement SwiftUI package and link its products.
Swift Package Manager
Storyly Placement SwiftUI is available through Swift Package Manager. To integrate it into your Xcode project, add and enter the package repository in the Swift Packages tab of your project, then link the PlacementSwiftUI product to your app target.
Info
This is the only package you add. It brings the Storyly Placement SDK (
StorylyPlacement,StorylyCore) along as its own dependency, so you do not addstoryly-placement-iosseparately.
Warning
Each widget type is a separate product on iOS. In addition to
PlacementSwiftUI, you must link the product of every widget you want the placement to be able to render:StorylyStoryBarSwiftUI,StorylyBannerSwiftUI,StorylyCanvasSwiftUI,StorylySwipeCardSwiftUI,StorylyVideoFeedSwiftUI. A widget whose product is not linked will not render, even when the Dashboard serves it.The
SwiftUIsuffix belongs to this package. The unsuffixed names (StorylyStoryBar,StorylyBanner, …) are the products ofstoryly-placement-ios; if you already have that package in your project, you can keep linking the widget products from there instead — both routes reach the same binaries.
CocoaPods
Storyly Placement SwiftUI is also available through CocoaPods. Add it to your Podfile:
pod 'StorylyPlacementSwiftUI'
The pod pulls the StorylyPlacement SDK pod automatically. Note that the pod is named StorylyPlacementSwiftUI but the module you import is PlacementSwiftUI — see Import Module.
Tip
You can find the underlying Storyly Placement SDK's release notes here. The SwiftUI wrapper has its own version line — its releases are the tags of the storyly-placement-swiftui package.
Warning
The SwiftUI module targets
iOS 16or higher. The underlying Storyly Placement SDK itself supportsiOS 13and higher — use the UIKitSTRPlacementViewdirectly if you must support those versions.
Import Module
You need to import the module in the files where you use Storyly Placement:
import PlacementSwiftUI // StorylyPlacementView
import StorylyPlacement // STRPlacementDataProvider, STRDelegate, STRProductDelegate
import StorylyCore // STRPlacementConfig, STRTheme, STRWidgetController, STRPayload
Warning
PlacementSwiftUIdoes not re-export the SDK. It gives youStorylyPlacementViewand nothing else, so every file that names anSTR…type must importStorylyPlacementandStorylyCoreas well.
Initialize Components
This section explains how to set up and connect the core Placement components: data provider, placement view, and delegate callbacks.
STRPlacementDataProvider
The data provider is caller-owned and must outlive view updates. SwiftUI recreates View structs on every update, so creating the provider inside a view's initializer or body would rebuild the provider — and reload the placement — every time the view is re-evaluated.
An ObservableObject store held as @StateObject is the recommended home: SwiftUI creates it once and keeps it alive for the lifetime of the view.
@MainActor
final class PlacementStore: ObservableObject {
let provider: STRPlacementDataProvider
init() {
provider = STRPlacementDataProvider()
provider.config = STRPlacementConfig.Builder()
.build(token: "<your_placement_token>")
}
}
struct HomeScreen: View {
@StateObject private var store = PlacementStore()
var body: some View {
StorylyPlacementView(dataProvider: store.provider)
}
}
Warning
Please login to Storyly dashboard and get your placement token.
Warning
A provider's token is fixed when its config is built. If you need to show a different placement token, you must create a new
STRPlacementDataProviderinstead of rebuilding the config on the existing one.
Tip
Do not declare the provider with
@Stateor as a plainletinside aView. Only@StateObject(or a provider owned further up the hierarchy and passed down) guarantees a single instance per screen.
Setup Widget Theme
This sections explains how to control the color theme of the widget rendered inside a Storyly Placement, so it blends in with your app's light or dark appearance.
You need to use setTheme to set the widget color theme via STRPlacementConfig.
provider.config = STRPlacementConfig.Builder()
.setTheme(theme: .dark) // or .light (default)
.build(token: "<your_placement_token>")
STRTheme is an enum with two values:
| Value | Appearance | When to use |
|---|---|---|
.light | Light theme (default) | Hosts with a light background, or app in light mode. |
.dark | Dark theme | Hosts with a dark background, or app in dark mode. |
StorylyPlacementView
This is the SwiftUI view that actually changes widgets in and out. You place it in your layout, pass it the STRPlacementDataProvider instance, and size it with layout modifiers.
struct HomeScreen: View {
@StateObject private var store = PlacementStore()
var body: some View {
StorylyPlacementView(dataProvider: store.provider)
.frame(maxWidth: .infinity)
.frame(height: 220)
}
}
| Parameter | Required | Description |
|---|---|---|
dataProvider | Yes | The caller-owned STRPlacementDataProvider supplying placement data. |
delegate | No | STRDelegate for ready, visibility, action-click, event and failure callbacks. |
productDelegate | No | STRProductDelegate for shoppable-content callbacks (cart, wishlist, product events). |
The view has no modifier parameter — it takes exactly the size SwiftUI proposes, so you drive its size with layout modifiers (.frame, .aspectRatio, …) applied to it.
Warning
The placement has no intrinsic size. If the layout does not give it a width and a height, nothing will be rendered. Either give it a fixed size, or derive its height from the ratio reported by
onWidgetReady— see Placement Size Handling.
Info
The SDK presents its fullscreen player from a view controller. The SwiftUI view resolves the active window's top-most view controller automatically, so you do not need to set
rootViewControlleryourself.
STRDelegate
Storyly Placement provides several delegate callbacks that allow your application to react to changes in widget state, user interactions, and analytics events.
To observe and respond to these behaviors, you must pass an STRDelegate to the StorylyPlacementView view.
Info
onWidgetReadyThis callback is triggered when the active widget ready to render. It gives a width-to-height ratio for the best rendering of view.
onVisibilityChangeThis callback is triggered when the placement view visibility should change based on widget data availability.
onActionClickedThis callback is triggered when the user interacts with the widget's action area (e.g., swipe-up or action button).
Callbacks are delivered outside of SwiftUI's view update, so they need somewhere to write into. The recommended pattern is a small ObservableObject that the delegate writes and your layout reads:
@MainActor
final class PlacementState: NSObject, ObservableObject, STRDelegate {
/// Width-to-height ratio reported by onWidgetReady; nil until a widget is ready.
@Published var ratio: CGFloat?
/// True once onWidgetReady has fired, i.e. ratio can be trusted.
@Published var isReady: Bool = false
/// Last value from onVisibilityChange — true while the placement has content to show.
@Published var isVisible: Bool = false
func onWidgetReady(widget: any STRWidgetController, ratio: CGFloat) {
self.ratio = ratio
self.isReady = true
}
func onVisibilityChange(widget: (any STRWidgetController)?, isVisible: Bool) {
self.isVisible = isVisible
if !isVisible {
isReady = false
ratio = nil
}
}
func onActionClicked(widget: any STRWidgetController, url: String, payload: STRPayload) {
// See Placement Action Handling below.
}
}
struct HomeScreen: View {
@StateObject private var store = PlacementStore()
@StateObject private var state = PlacementState()
var body: some View {
StorylyPlacementView(dataProvider: store.provider, delegate: state)
.frame(maxWidth: .infinity)
.frame(height: 220)
}
}
Warning
The SDK holds delegates weakly. You must keep your delegate alive yourself —
@StateObject(as above) or an object owned by your store. A delegate created inline in the view body is released immediately and no callback will ever fire.
Placement Size Handling
This section explains how to size the placement whenever the widget changes its size ratio.
The onWidgetReady callback notifies your application when the widget is ready to render with required ratio, allowing you to calculate and update the correct layout for the placement. In SwiftUI you feed that ratio straight into .aspectRatio, and collapse the placement to zero height until it arrives so an empty placement reserves no space.
extension View {
@ViewBuilder
func placementSize(_ state: PlacementState) -> some View {
if state.isReady, let ratio = state.ratio, ratio > 0, ratio.isFinite {
frame(maxWidth: .infinity).aspectRatio(ratio, contentMode: .fit)
} else {
frame(maxWidth: .infinity).frame(height: 0)
}
}
}
struct HomeScreen: View {
@StateObject private var store = PlacementStore()
@StateObject private var state = PlacementState()
var body: some View {
StorylyPlacementView(dataProvider: store.provider, delegate: state)
.placementSize(state)
}
}
Tip
Inside a
Listor aLazyVStack, reserve a fixed height instead of collapsing to0until the widget is ready. Otherwise the list jumps when the placement appears.
Info
In a lazy container, scrolling the placement off-screen can dispose the underlying view — its delegates are detached at that point — and scrolling back recreates and reloads it. This is expected; your
PlacementStatesurvives because it is owned by the row's@StateObject.
Warning
You must test with different widget types in the Dashboard's Placement page to verify that dynamic sizing behaves as expected. You can use this as a verification step in your initialization.
Placement Action Handling
This section shows how to handle Swipe Up and Action Button clicks from user.
The redirection needs to be handled by the application itself when the end-user clicks any action in the content. In order to handle this action, you must implement onActionClicked in your STRDelegate. You can handle the action using the following code example:
func onActionClicked(
widget: any STRWidgetController,
url: String,
payload: STRPayload
) {
print("onActionClicked: url=\(url), payload=\(payload)")
// Use widget.pause() to temporarily suspend widget activity
// Implement navigation via URL or handle product-specific actions here
// Use widget.resume() to restore functionality once navigation is complete
}
Warning
For StoryBar and VideoFeed, the fullscreen player is presented over your app's window. A
NavigationStackpush or state change you trigger fromonActionClickedhappens behind the player, so the user never sees it. Dismiss the player first — or record the destination in your state and navigate once the player is dismissed.
Warning
Please confirm
onActionClickedcallback is triggered upon any action in the content. Make sure your app navigates to correctly and check the logs to validate.
Placement Visibility Handling
This section explains how to handle the visibility of the placement based on whether there is a widget to display.
The onVisibilityChange callback notifies your application when the placement's visibility should change. This happens when the widget data is successfully loaded (isVisible=true) or when there is no data or a load failure (isVisible=false).
func onVisibilityChange(
widget: (any STRWidgetController)?,
isVisible: Bool
) {
// Update the visibility of the placement to match the state of the isVisible flag
}
Best Practices
- You must own the
STRPlacementDataProvideroutside the view struct (@StateObjectstore) so it is not rebuilt on every view update- You must keep your delegates alive yourself — the SDK references them weakly
- You must give the placement a size via layout modifiers — it has no intrinsic size
- You must honor
onWidgetReadyfor responsive UI- You must honor
onVisibilityChangefor visibility of placement- You must handle
onActionClickedfor navigation for action of end-user, and defer navigation until the fullscreen player is dismissed for StoryBar and VideoFeed- You must link the product of every widget type you want the placement to render
- You should handle
setThemefor consistent color theme look with your application.
Updated about 13 hours ago

