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 Surface

Placement 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 Point

It 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 Provider

It fetches and manages the content/config for a placement and exposes delegate callbacks. It is caller-owned and must outlive SwiftUI view updates.

Placement Config

It controls test mode, user/targeting context, and layout direction.

StorylyPlacement View

It 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 add storyly-placement-ios separately.

🚧

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 SwiftUI suffix belongs to this package. The unsuffixed names (StorylyStoryBar, StorylyBanner, …) are the products of storyly-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 16 or higher. The underlying Storyly Placement SDK itself supports iOS 13 and higher — use the UIKit STRPlacementView directly 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

PlacementSwiftUI does not re-export the SDK. It gives you StorylyPlacementView and nothing else, so every file that names an STR… type must import StorylyPlacement and StorylyCore as 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 STRPlacementDataProvider instead of rebuilding the config on the existing one.

📘

Tip

Do not declare the provider with @State or as a plain let inside a View. 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:

ValueAppearanceWhen to use
.lightLight theme (default)Hosts with a light background, or app in light mode.
.darkDark themeHosts 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)
    }
}
ParameterRequiredDescription
dataProviderYesThe caller-owned STRPlacementDataProvider supplying placement data.
delegateNoSTRDelegate for ready, visibility, action-click, event and failure callbacks.
productDelegateNoSTRProductDelegate 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 rootViewController yourself.

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

onWidgetReady

This callback is triggered when the active widget ready to render. It gives a width-to-height ratio for the best rendering of view.

onVisibilityChange

This callback is triggered when the placement view visibility should change based on widget data availability.

onActionClicked

This 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 List or a LazyVStack, reserve a fixed height instead of collapsing to 0 until 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 PlacementState survives 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 NavigationStack push or state change you trigger from onActionClicked happens 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 onActionClicked callback 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 STRPlacementDataProvider outside the view struct (@StateObject store) 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 onWidgetReady for responsive UI
  • You must honor onVisibilityChange for visibility of placement
  • You must handle onActionClicked for 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 setTheme for consistent color theme look with your application.

Did this page help you?