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

# Prefetching and readiness

> Warm up placements ahead of time so a flow appears instantly when you need it.

A flow normally resolves over the network the first time you present it, which can add a short delay. If you know a placement is coming (onboarding at launch, a paywall behind a known button), warm it up ahead of time so a later present is served from cache with no network round-trip.

There are three ways to warm placements:

* **`prefetch(_:warmMedia:)`** warms one or more placements on demand and tells you, per placement, exactly what is now ready.
* **`prefetchOnLaunch`** declares placements to warm automatically in the background right after `configure(...)`.
* **`isPlacementReady(_:)`** checks whether a flow is available, and leaves it warm as a side effect.

## prefetch

```swift theme={null}
@discardableResult
public func prefetch(
    _ placementKeys: [String],
    warmMedia: Bool? = nil
) async -> [String: PrefetchOutcome]
```

`prefetch` resolves each placement key and caches the result (the flow JSON, in memory and on disk) plus any custom fonts the flow uses, so a later present can serve from cache instead of waiting on the network. It runs the keys concurrently, de-duplicates the input, and **never throws**: instead of swallowing failures, it returns a `[String: PrefetchOutcome]` so you can see, per placement, what is warm and what failed, without a second round-trip.

`prefetch` is not `@MainActor`; call it from any async context (for example a launch `Task`).

```swift theme={null}
import FlowPilotSDK

// At app launch, after configure(...).
Task {
    let outcomes = await FlowPilot.shared?.prefetch(["onboarding", "paywall"])

    for (key, outcome) in outcomes ?? [:] {
        switch outcome.state {
        case .warmed(let fromCache):
            // A presentable flow is cached. `fromCache` is true when it came from
            // a local cached copy, false when freshly resolved or bundled.
            print("\(key): ready (fromCache: \(fromCache), media: \(outcome.mediaWarmed))")
        case .noFlow:
            // Resolve succeeded but the backend has nothing to show here.
            print("\(key): no flow")
        case .failed(let error):
            // Resolve failed and no fallback was available; nothing warmed.
            print("\(key): failed (\(error))")
        }
    }
}
```

The result is `@discardableResult`, so a bare `await prefetch([...])` (ignoring the return value) keeps working unchanged.

### Warming media

By default a bare `prefetch([...])` warms the flow JSON and fonts **only**, not images. To also warm images, opt in with `warmMedia: true`:

```swift theme={null}
await FlowPilot.shared?.prefetch(["onboarding"], warmMedia: true)
```

When `warmMedia` is `true`, how many images are warmed is bounded by [`prefetchMediaStrategy`](/ios-sdk/configuration) (first screen by default; see the table under [Prefetch at launch](#prefetch-at-launch)). Media warming is best-effort: a failed image download never fails the prefetch, and `PrefetchOutcome.mediaWarmed` tells you whether it ran. See [Media preloading](/ios-sdk/media-preloading) for how images are otherwise loaded at present time.

### PrefetchOutcome

```swift theme={null}
public struct PrefetchOutcome: Sendable {
    public enum State: Sendable {
        case warmed(fromCache: Bool)   // a presentable flow is cached and ready
        case noFlow                    // resolve ok, backend has nothing to show
        case failed(FlowPilotError)    // resolve failed; nothing warmed
    }
    public let placementKey: String
    public let state: State
    public let mediaWarmed: Bool       // first/all-screen images warmed
    public var isWarmed: Bool          // convenience: true when state is .warmed
}
```

| Field          | Meaning                                                                                                                                                      |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `placementKey` | The placement this outcome is for.                                                                                                                           |
| `state`        | `.warmed(fromCache:)`, `.noFlow`, or `.failed(_)`. See below.                                                                                                |
| `mediaWarmed`  | `true` when images were warmed into the image cache as part of this prefetch. Always `false` unless you requested media warming and the flow is presentable. |
| `isWarmed`     | Convenience: `true` when `state` is `.warmed`.                                                                                                               |

For `.warmed(fromCache:)`, `fromCache` is `true` when the flow came from a local cached copy (a fresh hit or a last-known-good stale entry) without a fresh network resolve, and `false` when it was freshly resolved from the network or served from a [bundled default](/ios-sdk/offline-bundled-flows).

## Prefetch at launch

Declare placements to warm automatically right after `configure(...)`. Warming runs in the background at utility priority and never blocks startup.

```swift theme={null}
FlowPilot.configure(
    FlowPilotConfiguration(
        apiKey: "fp_live_xxxxxxxxxxxxxxxx",
        appId: "your-app-id",
        prefetchOnLaunch: ["onboarding", "paywall"],
        prefetchMediaStrategy: .firstScreen   // .none | .firstScreen (default) | .allScreens
    )
)
```

Unlike a bare manual `prefetch([...])`, launch prefetch **does** warm images by default, governed by `prefetchMediaStrategy`:

| Strategy                 | Warms                                                                       |
| ------------------------ | --------------------------------------------------------------------------- |
| `.none`                  | flow JSON + fonts only                                                      |
| `.firstScreen` (default) | + first-screen and persistent-zone (navigation bar, footer, overlay) images |
| `.allScreens`            | + every screen's images                                                     |

With two or more placements declared, the SDK warms them in a single batch round-trip ([`POST /apps/{appId}/placements/resolve-batch`](/reference/sdk-rest-api)), then falls back to per-placement resolves if that endpoint is unavailable. Either way the result is the same warmed cache.

<Warning>
  Launch prefetch needs caching on and a non-zero TTL to do anything.

  * It is a **no-op when `cachingEnabled == false`** (nothing would be retained); the SDK logs a warning.
  * A warmed flow only lives as long as its freshness TTL (driven by the resolve response's `cache_ttl_seconds`). The `.development` and `.custom` environments disable HTTP caching, so against a backend that returns a `0` TTL a warmed entry expires immediately and prefetch has no visible effect. Use `.staging` / `.production` (or a backend that returns a non-zero TTL) to see the benefit. See [Caching](/ios-sdk/caching).
</Warning>

## isPlacementReady

```swift theme={null}
public func isPlacementReady(_ placementKey: String) async -> Bool
```

`isPlacementReady` returns `true` if a flow is available for the placement. It checks the cache first (a cache hit returns `true` without a network call); if there is no cache, it performs a live resolve. It is not `@MainActor` and never throws (a failed resolve returns `false`).

Crucially, the resolve it performs is **not wasted**: it routes through the same cache-populating path as `prefetch`, so a presentable flow is left warm and the following `presentPlacement` hits the cache with no second round-trip.

```swift theme={null}
if await FlowPilot.shared?.isPlacementReady("onboarding") == true {
    FlowPilot.shared?.presentPlacement("onboarding")   // served from cache
}
```

Use it to decide whether to show an entry point at all, for example only enabling an "Upgrade" button when a paywall flow exists.

```swift theme={null}
struct UpgradeButton: View {
    @State private var session: FlowSession?
    @State private var canShow = false

    var body: some View {
        Button("Upgrade") {
            Task {
                session = try? await FlowPilot.shared?.createSession(placementKey: "paywall")
            }
        }
        .disabled(!canShow)
        .flowPresenter(session: $session)
        .task {
            canShow = await FlowPilot.shared?.isPlacementReady("paywall") ?? false
        }
    }
}
```

## Identity-correct caching

A resolved flow is personalized by the user identity and targeting attributes it was resolved under. The cache reflects this: a warmed flow is only reused for the **same identity and attributes** it was prefetched with. If you change the user (for example after login) or update targeting attributes via [`updateContext`](/ios-sdk/variables-and-context), the next present resolves fresh rather than serving the previously warmed flow. This keeps targeting and experiment assignment correct: a flow warmed while anonymous is never shown to a since-identified user. The stale-cache offline fallback (Tier 2) still works across an identity change. See [Caching](/ios-sdk/caching).

## In-flight coalescing

Concurrent resolves for the same placement and identity share a single network call. If you `prefetch` a placement and then `presentPlacement` it before the prefetch finishes (or fire two prefetches for the same key), only one resolve goes to the network; the others await the same result. A caller hitting its own `resolveTimeout` does not cancel the shared resolve, so it still completes and populates the cache for the next caller.

## When to prefetch

* **At app launch**, with `prefetchOnLaunch`, for the first placement the user will hit (often `onboarding`).
* **Before a known entry point**, for example call `prefetch(["paywall"])` when the user lands on a screen that has an upgrade button.
* **Not everything.** Each key is a resolve (a network call when not cached). Prefetch only what you are reasonably likely to show.

## Notes and warnings

* **Best-effort, not a guarantee.** A `.warmed` outcome or a `true` from `isPlacementReady` is a strong hint, not a promise: conditions can change between the check and the present (context updates, experiment bucketing, a paused placement), so the present can still resolve differently. Always handle the "no flow" path at present time (catch the error or use the [`fallback`](/ios-sdk/presenting-placements) overload).
* **Readiness can be `false` offline.** If a placement is not cached and the device is offline, the live resolve fails and `isPlacementReady` returns `false`, even if a [bundled default](/ios-sdk/offline-bundled-flows) would let the present succeed.
* **Media warming is bounded and best-effort.** It respects the image cache's concurrency cap, skips images already cached, and never fails the prefetch. Lottie and video assets are not warmed.

## Common mistakes

* **Prefetching everything.** Each key triggers a resolve. Prefetch only the placements you expect to present soon.
* **Expecting a bare `prefetch` to warm images.** A bare `prefetch([...])` warms JSON and fonts only. Pass `warmMedia: true`, or use `prefetchOnLaunch` (which warms media per `prefetchMediaStrategy`), to remove first-screen image pop-in.
* **Assuming readiness means a flow will show.** `isPlacementReady` reflects the moment you call it. Still handle a missing flow when you present (use the fallback overload or catch the error).
* **Calling `isPlacementReady` repeatedly to poll.** Check it once at the point you need the decision. (The first call leaves the placement warm, so the following present is cheap.)
* **Expecting launch prefetch to work in development.** With `.development` (or a `0`-TTL backend), warmed entries expire immediately. Test launch prefetch against `.staging` / `.production`.

## Related pages

* [Configuration](/ios-sdk/configuration)
* [Caching](/ios-sdk/caching)
* [Media preloading](/ios-sdk/media-preloading)
* [Presenting placements (UIKit)](/ios-sdk/presenting-placements)
* [Offline and bundled flows](/ios-sdk/offline-bundled-flows)
* [SDK REST API](/reference/sdk-rest-api)
