Optimize paywall fetching in iOS SDK

A reliable paywall fetch on iOS does three things: renders fast, returns the audience-targeted paywall, and falls back gracefully when the network is slow. The rules below cover the timing, caching, and fallback patterns to get there.

Tip

The rules assume Adapty.activate() and Adapty.identify() have already resolved. See Call order in iOS SDK.

Rules and pitfalls

Do thisDon’t do thisWhy
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 getPaywall after attribution has had a chance to resolve — for example, 1–2 seconds after activate or after onProfileUpdate fires.Call getPaywall at App.init().Attribution hasn’t landed yet. The paywall 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 getPaywall indefinitely.Without a timeout, users on poor connectivity see a blank screen until the network resolves — or close the app.

See Fetch paywalls and products for the fetchPolicy and loadTimeout parameter reference, and Placements for picking the right placement.

Preload placements

Info

These methods are available starting from SDK version 4.1.

preloadFlows and preloadOnboardings fetch placements into the SDK cache ahead of time. A later getFlow or getOnboarding call for the same placement then resolves from cache instead of the network, so the paywall renders without a visible wait.

Use them 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 paywall behind a button the user hasn’t tapped.

Parameters:

  • placementIds (required): the placements to preload. Blank and duplicate IDs are ignored.
  • locale (optional, preloadOnboardings only): the onboarding locale to cache.
  • 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 methods throw 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 getFlow or getOnboarding to display it.

Find out which placement failed

The thrown error is a single AdaptyError covering the whole batch, with the code networkFailed (2002). 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”.

To warm the cache without waiting for audience segmentation at all, use the default-audience variants:

try await Adapty.preloadFlowsForDefaultAudience(placementIds: ["main_paywall"])
try await Adapty.preloadOnboardingsForDefaultAudience(placementIds: ["intro"])

Tune for poor connectivity

For markets with consistently poor connectivity (rural areas, transit, regions affected by routing):

  • Set fetchPolicy: .returnCacheDataElseLoad on every fetch except the very first.
  • Configure a fallback paywall for every placement in the Adapty dashboard.
  • Set loadTimeout to 3–5 seconds and accept the fallback when the timeout fires.
  • Don’t gate paywall display on getProfile(). Call getPaywall independently so a slow profile doesn’t block the UI.