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

# Presenting placements

> Resolve and present the flow for a placement key from a UIKit view controller.

To show a flow in a UIKit app, you ask FlowPilot to resolve and present a placement key from a view controller. The SDK works out which flow to show (cache, network, experiment variant, audience) by calling the [resolve endpoint](/reference/sdk-rest-api), renders it natively on top of your view controller, and hands you back a [`FlowResult`](/ios-sdk/results-and-outcomes) when the user finishes.

All of the present methods are declared `@MainActor`, so call them from the main actor (for example from a view controller method, inside a `Task {}`). They are only available where UIKit is (`#if canImport(UIKit)`); for SwiftUI, see [SwiftUI integration](/ios-sdk/swiftui).

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

## The three present overloads

`FlowPilot` exposes three `presentPlacement` overloads. They all resolve and present the same way; they differ in how they hand back the result and how they behave when there is no flow.

| Overload                                       | Returns                                        | Throws?                | Use when                                                                                                               |
| ---------------------------------------------- | ---------------------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `presentPlacement(_:from:options:)`            | `FlowResult`                                   | `async throws`         | You want the result back with `await` and will handle the "no flow" error.                                             |
| `presentPlacement(_:from:options:completion:)` | `Void` (delivers `FlowResult` to `completion`) | `async throws`         | You prefer a callback over an awaited return value.                                                                    |
| `presentPlacement(_:from:fallback:options:)`   | `FlowResult`                                   | `async` (never throws) | Onboarding and other "must not fail" entry points. Presents your own view controller if FlowPilot has nothing to show. |

### Awaited result (throws)

```swift theme={null}
@MainActor
public func presentPlacement(
    _ placementKey: String,
    from viewController: UIViewController,
    options: PresentationOptions? = nil
) async throws -> FlowResult
```

This is the most direct form. It throws a [`FlowPilotError`](/ios-sdk/error-handling) when no flow could be resolved (no default flow, audience mismatch, offline with no cache, and so on), so wrap it in `do/catch`.

### Callback variant (throws)

```swift theme={null}
@MainActor
public func presentPlacement(
    _ placementKey: String,
    from viewController: UIViewController,
    options: PresentationOptions? = nil,
    completion: ((FlowResult) -> Void)? = nil
) async throws
```

Functionally the same as the awaited form, but the `FlowResult` is delivered to `completion` after the flow dismisses instead of being returned. You still `try await` the call.

### Never-throws fallback variant

```swift theme={null}
@MainActor
@discardableResult
public func presentPlacement(
    _ placementKey: String,
    from viewController: UIViewController,
    fallback: @escaping @MainActor () -> UIViewController,
    options: PresentationOptions? = nil
) async -> FlowResult
```

This overload never throws. It walks the full fail-safe chain (fresh cache, then a live resolve bounded by `resolveTimeout`, then stale cache, then a bundled default flow). If none of those produce a presentable flow, it presents the view controller you return from `fallback` (your app's own native onboarding) instead. The `fallback` closure runs on the main actor.

When it has to fall back to your view controller, the returned `FlowResult` has `outcome == .error` and a populated `error`. This is the resilient choice for onboarding, because integrating FlowPilot can never leave the user on a blank screen. See the [fail-safe fallback chain](/ios-sdk/offline-bundled-flows).

## Presentation options

Pass a `PresentationOptions` to add per-presentation context or turn off the present animation.

```swift theme={null}
public struct PresentationOptions: Sendable {
    public let additionalContext: SDKContext?     // merged for this presentation only
    public let presentationStyle: PresentationStyle?
    public let animated: Bool                       // default true

    public init(
        additionalContext: SDKContext? = nil,
        presentationStyle: PresentationStyle? = nil,
        animated: Bool = true
    )
}

public enum PresentationStyle: String, Sendable {
    case fullScreen
    case modal
    case bottomSheet
}
```

* **`additionalContext`** (`SDKContext` is `[String: Any]`) is merged on top of the SDK-wide context for this presentation only. Use it to pass values the flow can read as App Parameter variables (for example which button opened a paywall). See [Variables and SDK context](/ios-sdk/variables-and-context).
* **`animated`** controls whether the modal present and dismiss are animated. Defaults to `true`.

<Warning>
  `presentationStyle` is accepted by the initializer but is **not applied** by the current SDK. Flows always present full screen (`FlowHostingController` sets `.fullScreen`). Do not rely on `.modal` or `.bottomSheet` changing the presentation.
</Warning>

## Example

Present from `viewDidAppear`, inside a `Task`, with all three styles shown.

```swift theme={null}
import UIKit
import FlowPilotSDK

final class HomeViewController: UIViewController {
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        Task {
            guard let flowPilot = FlowPilot.shared else { return }

            // 1. Awaited result, with extra context for this presentation.
            do {
                let options = PresentationOptions(
                    additionalContext: ["paywall_source": "home_banner"]
                )
                let result = try await flowPilot.presentPlacement(
                    "paywall",
                    from: self,
                    options: options
                )
                handle(result)
            } catch {
                // No flow could be resolved. Decide what your app does here.
                print("FlowPilot could not present a flow: \(error)")
            }
        }
    }

    func handle(_ result: FlowResult) {
        // See "Results and outcomes" for the full FlowResult shape.
        switch result.outcome {
        case .completed: print("User finished the flow")
        case .dismissed: print("User closed the flow early")
        case .error: print("Error: \(result.error?.message ?? "unknown")")
        }
    }
}
```

For onboarding, prefer the never-throws fallback overload so the user always sees something:

```swift theme={null}
Task {
    let result = await FlowPilot.shared?.presentPlacement(
        "onboarding",
        from: self,
        fallback: { MyNativeOnboardingViewController() }
    )
    // result?.outcome == .error means FlowPilot fell back to your native UI.
}
```

## Common mistakes

* **Calling off the main thread.** The present methods are `@MainActor`. Call them from the main actor (UIKit lifecycle methods already run there). A background `Task` that calls them without hopping to the main actor will not compile cleanly or will assert.
* **Not awaiting the call.** `presentPlacement` is `async`. Wrap it in a `Task {}` and `await` it; do not call it and walk away.
* **Presenting before `configure`.** `FlowPilot.shared` is `nil` until `FlowPilot.configure(_:)` runs. Guard `FlowPilot.shared` or your call is a silent no-op.
* **Not handling the "no flow" case.** The throwing overloads throw when there is nothing to show. Either `catch` it, use the `fallback:` overload, or check [`isPlacementReady`](/ios-sdk/prefetching) first.
* **Expecting `presentationStyle` to change the modal.** It does not (see the warning above). Flows present full screen today.

## Troubleshooting

The throwing overloads threw, or the fallback overload returned `.error`:

| Symptom                             | Likely cause                                                 | Fix                                                                                                              |
| ----------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
| `flow_not_found` error              | No published flow attached to the placement, or it is paused | Attach and publish a flow, set the placement active. See [Placements](/dashboard/placements).                    |
| Nothing resolves, no error offline  | Device offline with no cache and no bundled default          | Ship a [bundled default flow](/ios-sdk/offline-bundled-flows) or pre-warm with [prefetch](/ios-sdk/prefetching). |
| `timeout` error                     | Resolve exceeded `resolveTimeout` (default 4s)               | Network is slow; the SDK already falls back to cache/bundled. Raise `resolveTimeout` only if needed.             |
| Resolves on one device, not another | Audience targeting excludes this user/context                | Check the placement's audience rules and the context you pass.                                                   |
| Always empty for one app            | Wrong `appId`, the placement belongs to another app          | Configure the `appId` that owns the placement.                                                                   |

See [Error handling](/ios-sdk/error-handling) for every error code.

## Related pages

* [SwiftUI integration](/ios-sdk/swiftui)
* [Results and outcomes](/ios-sdk/results-and-outcomes)
* [Prefetching and readiness](/ios-sdk/prefetching)
* [Placements (dashboard)](/dashboard/placements)
* [Error handling](/ios-sdk/error-handling)
* [Configuration](/ios-sdk/configuration)
* [SDK REST API (resolve contract)](/reference/sdk-rest-api)
