iOS SDK (Swift) / Rendering flows
Rendering flows
One view. It fetches the flow by key, caches it, renders every screen as SwiftUI, and calls you back with the answers.
The view
import SwiftUI
import UpliftFunnel
struct OnboardingScreen: View {
var body: some View {
UpliftFunnelFlowView(flowKey: "welcome") { result in
print(result.endReason) // "completed" / "abandoned" / "skipped"
print(result.variables) // every answer the user gave
print(result.source) // network / cache / revalidated
}
}
}| Parameter | Type | Default | Description |
|---|---|---|---|
| flowKey | String | — | The flow's slug, from the dashboard. |
| onCompleted | ((UpliftFunnelFlowResult) -> Void)? | nil | Called once, when the flow ends for any reason. |
| userVariables | [String: JSONValue]? | nil | Values your app already knows, seeded into the session before the first screen. |
| forceRefresh | Bool | false | Skip the cache and refetch. Normal starts are cache-first. |
| loadingView | (() -> AnyView)? | nil | Replaces the default spinner. |
| errorView | ((Error, @escaping () -> Void) -> AnyView)? | nil | Replaces the default error state. The closure's second argument retries the fetch. |
What comes back
| Field | Type | Description |
|---|---|---|
| endReason | String | Why it ended: "completed", "abandoned", "skipped", or a custom reason the flow authored. |
| variables | [String: JSONValue] | Every answer collected over the session. |
| source | FetchSource | network, cache, or revalidated — useful for surfacing offline state. |
| experiment | UpliftFunnelExperimentAssignment? | The A/B assignment this session ran under, or nil. |
Seeding what you already know
A flow should not ask for a name your app has. Pass it in, and the screens can interpolate it and branch on it from the first frame.
UpliftFunnelFlowView(
flowKey: "welcome",
onCompleted: { result in save(result.variables) },
// Anything your app already knows. Available to the flow as
// {{first_name}} and to its transition conditions.
userVariables: [
"first_name": .string(user.firstName),
"is_subscriber": .bool(user.isSubscriber)
]
)Your own loading and error states
The defaults are a spinner and a message with a Retry button. Replace either when the flow is the first thing a user sees and the default would look like a different app.
UpliftFunnelFlowView(
flowKey: "welcome",
onCompleted: handle,
loadingView: { AnyView(MySplash()) },
errorView: { error, retry in
AnyView(MyErrorView(message: error.localizedDescription, retry: retry))
}
)Running an experiment
Same view, routed through the experiments endpoint: the SDK attaches a sticky subject id, the server buckets the user into a stable variant, and the assignment comes back on the result.
UpliftFunnelFlowView.experiment("paywall-copy-test") { result in
// Typed assignment — nil for a flow with no running experiment.
let variant = result.experiment
analytics.log("onboarding_done", [
"experiment_id": variant?.experimentId,
"variant_id": variant?.variantId,
"variant_name": variant?.variantName
])
}Safe to leave in production after a decision — a stopped experiment serves the baseline and a rolled-out one serves the winner. More on experiments →
Starting a session yourself
When you want the fetch to happen before the view exists — during a splash, say — start it directly and render the session later.
// Start it early — on app launch, behind your splash — and render later.
let start = try await UpliftFunnel.start("welcome", userVariables: seed)
print(start.source) // where the JSON came from
print(start.flowVersion) // the published version that was rendered
print(start.experiment) // assignment, if any
// Then render the session you already have.
UpliftFunnelSessionView(session: start.session) { variables, reason in
save(variables)
}UpliftFunnel.startExperiment(_:) is the experiment equivalent and returns the same FlowSessionStart.