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

# Media preloading

> Preload a flow's images before showing it so users never see loading spinners mid-flow.

Media preloading downloads a flow's images ahead of time, in screen order, so each screen is ready before the user reaches it. Without it, an image-heavy screen can flash a placeholder while it loads. With it, the flow feels instant.

Preloading happens automatically in the background when a session is created. You do not have to wait for it. You can observe its progress and, if you want, gate the present until it finishes.

## Turning preloading on or off

Preloading is on by default, controlled in two places:

<ParamField path="mediaPreloadingEnabled" type="Bool" default="true">
  Set in [`FlowPilotConfiguration`](/ios-sdk/configuration). The default for every session. Images and media are downloaded in screen order when a flow initializes.
</ParamField>

<ParamField path="preloadMedia" type="Bool?" default="nil">
  A per-session override on `createSession(placementKey:additionalContext:preloadMedia:)`. Pass `true` or `false` to override the configuration for one session; leave it `nil` to inherit the configuration default.
</ParamField>

```swift theme={null}
// Override for a single SwiftUI session.
let session = try await FlowPilot.shared?.createSession(
    placementKey: "onboarding",
    preloadMedia: true
)
```

When preloading is disabled, the flow still works; images just load on demand as each screen appears.

## Observing progress

A [`FlowSession`](/ios-sdk/swiftui) publishes two read-only properties you can observe (it is an `ObservableObject`):

<ParamField path="mediaPreloadProgress" type="Double">
  Download progress from `0.0` to `1.0`.
</ParamField>

<ParamField path="isMediaPreloaded" type="Bool">
  `true` once preloading has finished (or was disabled). It becomes `true` even if some downloads failed, so it means "preloading is done", not "everything succeeded".
</ParamField>

Two methods let you control timing:

* **`waitForMediaPreload()`** is `async` and suspends until preloading completes. Awaiting it does not block a thread, it suspends the calling task. Use it to gate a present on a fully preloaded flow.
* **`cancelMediaPreload()`** stops an in-progress preload, for example if the user navigates away before you present.

## Example

### Gate the present on a fully preloaded flow

Create the session (which starts preloading), wait for it, then present. The short upfront wait buys a flow with no mid-flow loading states:

```swift theme={null}
import FlowPilotSDK

func presentOnboarding(from viewController: UIViewController) {
    Task {
        guard let session = await FlowPilot.shared?.resolveSession(placementKey: "onboarding") else {
            // Nothing to present (see error handling / host fallback).
            return
        }

        // Suspend (does not block) until images are ready.
        await session.waitForMediaPreload()

        let host = FlowHostingController(session: session)
        viewController.present(host, animated: true)
    }
}
```

### Show a loading bar bound to progress (SwiftUI)

Observe `mediaPreloadProgress` on the session and swap to the flow once `isMediaPreloaded` is `true`:

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

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

    var body: some View {
        Group {
            if let session {
                PreloadGate(session: session)
            } else {
                ProgressView("Preparing...")
            }
        }
        .task {
            session = await FlowPilot.shared?.resolveSession(placementKey: "onboarding")
        }
    }
}

private struct PreloadGate: View {
    @ObservedObject var session: FlowSession

    var body: some View {
        if session.isMediaPreloaded {
            FlowPresenterView(session: session)
        } else {
            VStack(spacing: 12) {
                ProgressView(value: session.mediaPreloadProgress)
                Text("\(Int(session.mediaPreloadProgress * 100))%")
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }
            .padding()
        }
    }
}
```

## Notes

* **Preloading is in screen order.** Images on the first screen download first, then the second, and so on. Persistent zones (navigation bar, footer, overlay) are highest priority because they show on every screen. So even a partial preload covers what the user sees first.
* **It only covers images.** The preloader downloads image content. It does not preload Lottie animations or other media types.
* **Gating is a trade-off.** Waiting for a full preload means a short, predictable delay before the flow appears, in exchange for no spinners during the flow. Not gating means the flow appears immediately and the last screens may still be loading. Pick per placement.
* **Prefetch is related but separate.** A bare [`prefetch`](/ios-sdk/prefetching) warms the resolved-flow cache and fonts, not images; this media preloader runs at session creation or present time. They are complementary. You can also warm images ahead of time with `prefetch(_:warmMedia: true)` or `prefetchOnLaunch` (bounded by `prefetchMediaStrategy`), which uses this same image cache, so a placement warmed that way shows its first screen with no pop-in.

## Common mistakes

* **Blocking the main thread to wait.** Do not spin on `isMediaPreloaded` in a loop or run preloading synchronously on the main actor. Use `await session.waitForMediaPreload()` (which suspends, not blocks) or observe `mediaPreloadProgress` from SwiftUI.
* **Gating heavy media on cellular.** A flow with many large images can be slow to fully preload on a cellular connection. If you gate the present, the user waits. Consider not gating, or trimming media, for flows that run on first launch.
* **Assuming `isMediaPreloaded` means success.** It flips to `true` when preloading finishes, even if some images failed to download. A failed image simply loads (or retries) on demand later; it does not block the flow.
* **Recreating the session to retry preloading.** Preloading starts once per session at creation. To re-run it, create a new session rather than calling preload methods again.

## Troubleshooting

| Symptom                                  | Likely cause                                     | Fix                                                                                                                      |
| ---------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| Images still flash a placeholder         | Preloading disabled, or the present is not gated | Set `mediaPreloadingEnabled: true`, or `await waitForMediaPreload()` before presenting                                   |
| Long delay before the flow appears       | Gating a present on a flow with heavy media      | Drop the gate, or reduce image sizes in the flow                                                                         |
| `mediaPreloadProgress` never reaches 1.0 | Some images failed to download                   | Expected; `isMediaPreloaded` still flips to `true` and those images load on demand. Check the network and the image URLs |
| UI freezes while preparing               | Waiting on the main thread incorrectly           | Move the wait into a `Task` and `await` it                                                                               |

## Related pages

* [Caching](/ios-sdk/caching)
* [Offline and bundled flows](/ios-sdk/offline-bundled-flows)
* [Prefetching and readiness](/ios-sdk/prefetching)
* [SwiftUI integration](/ios-sdk/swiftui)
* [Configuration](/ios-sdk/configuration)
