Skip to main content
In SwiftUI you present a flow by creating a FlowSession and binding it to the .flowPresenter view modifier. Setting the binding presents the flow; the SDK clears it and calls your onResult closure when the flow ends. You can also embed a FlowPresenterView directly when you want the flow inline in your own view hierarchy.
The SDK must be configured first. FlowPilot.shared is nil until you call FlowPilot.configure(_:) at launch. See Configuration.

Create a session

There are two ways to get a FlowSession, both on FlowPilot and both @MainActor.
@MainActor
public func createSession(
    placementKey: String,
    additionalContext: SDKContext? = nil,
    preloadMedia: Bool? = nil
) async throws -> FlowSession

@MainActor
public func resolveSession(
    placementKey: String,
    additionalContext: SDKContext? = nil,
    preloadMedia: Bool? = nil
) async -> FlowSession?
  • createSession throws a FlowPilotError when no flow can be resolved. Use it when a missing flow is an error you want to handle.
  • resolveSession is the non-throwing counterpart. It walks the full fallback chain (fresh cache, live resolve with a hard timeout, stale cache, bundled default) and returns nil when FlowPilot has nothing to present, at which point you render your own native UI. Use it when a missing flow is a normal no-op.
Both accept:
  • additionalContext (SDKContext is [String: Any]): values merged on top of the SDK-wide context for this session only, readable by the flow as App Parameter variables. See Variables and SDK context.
  • preloadMedia: override the configured media preloading for this session. Defaults to the value set in Configuration. See Media preloading.

Present with the .flowPresenter modifier

extension View {
    public func flowPresenter(
        session: Binding<FlowSession?>,
        onResult: ((FlowResult) -> Void)? = nil
    ) -> some View
}
Bind it to an optional FlowSession state. Setting that state to a session presents the flow full screen over your UI; when the flow ends, the SDK sets the binding back to nil and calls onResult with the FlowResult.
import SwiftUI
import FlowPilotSDK

struct UpgradeButton: View {
    @State private var session: FlowSession?

    var body: some View {
        Button("Upgrade") {
            Task {
                // A missing flow here is an error we ignore (try?).
                session = try? await FlowPilot.shared?.createSession(
                    placementKey: "paywall",
                    additionalContext: ["source": "upgrade_button"]
                )
            }
        }
        .flowPresenter(session: $session) { result in
            if result.outcome == .completed {
                // Unlock the feature, refresh entitlements, etc.
            }
        }
    }
}
Retain the session in @State (or another stable store). A FlowSession created into a local variable is released as soon as the function returns, and the flow will not present. The binding must point at storage that outlives the call.

Embed a FlowPresenterView directly

When you want the flow inline rather than presented modally, build a FlowPresenterView from a session and place it in your hierarchy.
public struct FlowPresenterView: View {
    public init(session: FlowSession)
}
This pairs well with resolveSession, which lets you fall back to your own view when there is no flow:
struct OnboardingHost: View {
    @State private var session: FlowSession?
    @State private var didResolve = false

    var body: some View {
        Group {
            if let session {
                FlowPresenterView(session: session)
            } else if didResolve {
                MyNativeOnboardingView()   // no FlowPilot flow available
            } else {
                ProgressView()             // still resolving
            }
        }
        .task {
            session = await FlowPilot.shared?.resolveSession(placementKey: "onboarding")
            didResolve = true
        }
    }
}
FlowPresenterView starts navigation itself when it appears, so you do not call startNavigation() by hand. To learn the result when embedding, read the session’s published state (below) or await session.waitForCompletion().

Hosting in UIKit

For UIKit apps that want to host the SwiftUI presenter directly (rather than calling presentPlacement), FlowHostingController is a public UIHostingController subclass:
let host = FlowHostingController(session: session)
host.onCompletion { result in /* ... */ }
viewController.present(host, animated: true)
Most UIKit apps should just call presentPlacement, which uses this controller internally.

Observing FlowSession state

FlowSession is an ObservableObject. These published properties are public and useful to drive UI:
PropertyTypeMeaning
currentScreenScreenNode?The screen currently shown. currentScreen?.id and currentScreen?.name are available. nil before the first screen appears.
isActiveBooltrue while the flow is presenting, false once it has finished.
mediaPreloadProgressDoubleMedia preload progress, 0.0 to 1.0.
isMediaPreloadedBooltrue once media preloading has finished (or was disabled).
struct PreloadGate: View {
    @ObservedObject var session: FlowSession

    var body: some View {
        if session.isMediaPreloaded {
            FlowPresenterView(session: session)
        } else {
            ProgressView(value: session.mediaPreloadProgress)
        }
    }
}

Common mistakes

  • Not retaining the session. Store it in @State (or an @StateObject-owned model), not a local variable. See the warning above.
  • Using createSession where a missing flow is normal. createSession throws, so an absent flow becomes an error path. Use resolveSession (returns nil) when “no flow” should just fall through to your own UI.
  • Blocking the main thread on media preload. Preloading runs in the background. If you choose to wait for it (session.waitForMediaPreload()), do it in a Task, and show progress with mediaPreloadProgress rather than freezing the UI. See Media preloading.
  • Calling createSession / resolveSession off the main actor. Both are @MainActor. Call them from a .task {}, a Button action’s Task {}, or another main-actor context.