> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getflowpilot.io/llms.txt
> Use this file to discover all available pages before exploring further.

# SwiftUI integration

> Present FlowPilot flows declaratively with a FlowSession and the .flowPresenter modifier, or embed a FlowPresenterView directly.

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.

<Note>
  The SDK must be configured first. `FlowPilot.shared` is `nil` until you call `FlowPilot.configure(_:)` at launch. See [Configuration](/ios-sdk/configuration).
</Note>

## Create a session

There are two ways to get a `FlowSession`, both on `FlowPilot` and both `@MainActor`.

```swift theme={null}
@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`](/ios-sdk/error-handling) 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](/ios-sdk/variables-and-context).
* **`preloadMedia`**: override the configured media preloading for this session. Defaults to the value set in [Configuration](/ios-sdk/configuration). See [Media preloading](/ios-sdk/media-preloading).

## Present with the .flowPresenter modifier

```swift theme={null}
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`](/ios-sdk/results-and-outcomes).

```swift theme={null}
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.
            }
        }
    }
}
```

<Warning>
  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.
</Warning>

## 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.

```swift theme={null}
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:

```swift theme={null}
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`](/ios-sdk/presenting-placements)), `FlowHostingController` is a public `UIHostingController` subclass:

```swift theme={null}
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:

| Property               | Type          | Meaning                                                                                                                         |
| ---------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `currentScreen`        | `ScreenNode?` | The screen currently shown. `currentScreen?.id` and `currentScreen?.name` are available. `nil` before the first screen appears. |
| `isActive`             | `Bool`        | `true` while the flow is presenting, `false` once it has finished.                                                              |
| `mediaPreloadProgress` | `Double`      | Media preload progress, `0.0` to `1.0`.                                                                                         |
| `isMediaPreloaded`     | `Bool`        | `true` once media preloading has finished (or was disabled).                                                                    |

```swift theme={null}
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](/ios-sdk/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.

## Related pages

* [Presenting placements (UIKit)](/ios-sdk/presenting-placements)
* [Results and outcomes](/ios-sdk/results-and-outcomes)
* [Media preloading](/ios-sdk/media-preloading)
* [Variables and SDK context](/ios-sdk/variables-and-context)
* [Configuration](/ios-sdk/configuration)
