Optimize flow & paywall fetching in iOS SDK
A reliable flow or paywall fetch on iOS does three things: renders fast, returns the audience-targeted variation, and falls back gracefully when the network is slow. The rules below cover the timing, caching, and fallback patterns to get there.
The rules assume Adapty.activate() and Adapty.identify() have already resolved. See Call order in iOS SDK.
Rules and pitfalls
| Do this | Don’t do this | Why |
|---|---|---|
Fetch the placement you’re about to show, or warm the cache with preloadFlows (SDK 4.1+). | Fan out your own concurrent getFlow calls on launch. | A hand-rolled prefetch burst blocks the main thread and produces a black screen. preloadFlows is built for this and shares one timeout budget across the batch. |
Fetch getFlow after attribution has had a chance to resolve — for example, 1–2 seconds after activate or after didLoadLatestProfile fires. | Call getFlow at App.init(). | Attribution hasn’t landed yet. The flow resolves against the default audience and silently bypasses segments and ASA personalization. |
Set a loadTimeout and configure a fallback paywall for every placement. | Wait on getFlow indefinitely. | Without a timeout, users on poor connectivity see a blank screen until the network resolves — or close the app. |
When loadTimeout fires on any fetch — a plain getFlow included — the SDK returns the cached variation if one exists, and otherwise fetches the default-audience (All Users) variation within the remaining time. Targeting is lost for that request, not delayed: segments and attribution-based audiences don’t apply to the result.
See Fetch paywalls and products for the fetchPolicy and loadTimeout parameter reference, and Placements for picking the right placement.
Preload placements
preloadFlows and preloadFlowsForDefaultAudience are available starting from SDK version 4.1.
preloadFlows caches the flow JSON ahead of time — one request per placement. You then use it as usual: getFlow for the flow, getFlowConfiguration for its view configuration.
fetchPolicy decides which layer a later getFlow reads first, not whether it can reach the cache at all:
.returnCacheDataElseLoadreads the preloaded copy first and goes to the network only when nothing is cached..returnCacheDataIfNotExpiredElseLoad(maxAge:)does the same while the copy is younger thanmaxAge.- The default,
.reloadRevalidatingCacheData, goes to the network first and falls back to the preloaded copy when the request fails or times out.
A preload pays off either way, but differently: a cache-first policy removes the request, while the default keeps it and gains a warm copy to fall back on.
Use it when you know which placements the session will need but don’t want to show them yet — for example, right after activate and identify resolve, for the flow behind a button the user hasn’t tapped.
Parameters:
placementIds(required): the placements to preload. Blank and duplicate IDs are ignored.loadTimeout(optional): timeout in seconds for the whole batch, not per placement. Defaults to 5 seconds, and values below 1 second are raised to 1 second.
Behavior worth knowing:
- The method throws only after attempting every placement, and the error aggregates the per-placement failures. A failure for one placement doesn’t stop the others.
- If a placement times out or fails with a network error, the SDK falls back to the default-audience variation for that placement. Other failures are reported as-is.
- If the timeout fires before the audience-targeted fetch completes, the SDK still attempts the default-audience variation within the remaining budget.
- Preloading only warms the cache. It doesn’t return content — you still call
getFlowto display it.
What a preload covers
A flow reaches the screen in layers. A preload covers the first, exactly as getFlow does:
| Layer | Fetched by | Warmed by a preload |
|---|---|---|
| Flow JSON — the chosen variation, its product IDs, and the remote config | getFlow | Yes |
| UI layout — the screen’s structure, styling, and text | getFlowConfiguration | No |
| Images, including the still frame that stands in for a video element | getFlowConfiguration, in the background | No |
| Video files | The system player, as the screen renders | Not cached by the SDK |
getFlowConfiguration awaits the layout, so the first request for a given layout costs a round trip even after a preload. The SDK then holds that layout in its own disk cache, which survives app restarts and is read before any network call, so the cost falls on the first request rather than on every one. Once the SDK has the layout, it starts caching the images independently of the call: it doesn’t hold up the screen, and there’s no callback, delegate method, or error that reports when it finishes.
Find out which placement failed
The thrown error is a single AdaptyError covering the whole batch, with the code networkFailed (2005). To see the individual failures, read its preloadErrors property — a dictionary keyed by placement ID:
do {
try await Adapty.preloadFlows(placementIds: ["onboarding", "main_paywall"])
} catch {
for (placementId, placementError) in error.preloadErrors ?? [:] {
// log or retry the individual placement
}
}
preloadErrors is nil for any error that didn’t come from a preload call, so treat a nil value as “not a preload failure” rather than “no failures”.
Skip audience segmentation
To warm the cache without waiting for audience segmentation at all, use the default-audience variant:
try await Adapty.preloadFlowsForDefaultAudience(placementIds: ["main_paywall"])
Show first-screen media from the app bundle
A flow downloads its images and videos from Adapty. To show the first screen’s media instantly, serve it from the app bundle instead. This is a good way to reuse media you already ship, such as the visuals of an existing native onboarding.
- In the Flow & Paywall Builder, set a custom media ID on the image or video. The file you upload there stays as the fallback.
- Add the file to your app bundle.
- When you call
getFlowConfiguration, pass the bundled file for that ID throughassetsResolver:
// "welcome_video" is the custom media ID set in the Flow & Paywall Builder
let bundledAssets: [String: AdaptyCustomAsset] = [
"welcome_video": .video(
.file(
url: Bundle.main.url(forResource: "welcome", withExtension: "mp4")!,
preview: .uiImage(value: UIImage(named: "welcome_poster")!),
resolution: CGSize(width: 1080, height: 1920)
)
),
]
let flowConfig = try await AdaptyUI.getFlowConfiguration(
forFlow: flow,
assetsResolver: bundledAssets
)
Bundled files add to your app’s download size, so bundle only the media users see first.
Media you don’t bundle still appears right away: the view configuration carries a small low-resolution copy of each image, including a video’s still frame, and shows it until the full file loads.
For the full assetsResolver reference, see Customize assets.
Tune for poor connectivity
For markets with consistently poor connectivity (rural areas, transit, regions affected by routing):
- Set
fetchPolicy: .returnCacheDataElseLoadon every fetch except the very first. - Configure a fallback paywall for every placement in the Adapty dashboard.
- Set
loadTimeoutto 3–5 seconds and accept the fallback when the timeout fires. - Don’t gate flow display on
getProfile(). CallgetFlowindependently so a slow profile doesn’t block the UI.