Skip to main content
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:
mediaPreloadingEnabled
Bool
default:"true"
Set in FlowPilotConfiguration. The default for every session. Images and media are downloaded in screen order when a flow initializes.
preloadMedia
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.
// 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 publishes two read-only properties you can observe (it is an ObservableObject):
mediaPreloadProgress
Double
Download progress from 0.0 to 1.0.
isMediaPreloaded
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”.
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:
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:
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 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

SymptomLikely causeFix
Images still flash a placeholderPreloading disabled, or the present is not gatedSet mediaPreloadingEnabled: true, or await waitForMediaPreload() before presenting
Long delay before the flow appearsGating a present on a flow with heavy mediaDrop the gate, or reduce image sizes in the flow
mediaPreloadProgress never reaches 1.0Some images failed to downloadExpected; isMediaPreloaded still flips to true and those images load on demand. Check the network and the image URLs
UI freezes while preparingWaiting on the main thread incorrectlyMove the wait into a Task and await it