Prepare Journeys on iOS
Preload a Journey for instant SwiftUI or UIKit presentation.
FounderHQJourneys can prepare a published Journey before the user opens it.
Preparation loads the Journey configuration and its renderer in parallel, then
keeps one hidden WKWebView ready for presentation.
Before you start
Install FounderHQJourneys 0.8.0 or later. You need a published Journey, its
Journey ID, and a publishable key (fhq_pk_XXXX).
Prepare from SwiftUI
Create one JourneyHost for the screen or flow that presents the Journey. Keep
that host alive while the user can open, close, and reopen the Journey.
import FounderHQJourneys
import SwiftUI
struct OnboardingEntryView: View {
@State private var showJourney = false
@StateObject private var host = JourneyHost(configuration: .init(
apiKey: "fhq_pk_XXXX",
journeyID: "journey_123",
identity: JourneyIdentity(externalID: "usr_1042")
))
var body: some View {
Button("Start onboarding") {
showJourney = true
}
.task {
try? await host.prepare()
}
.fullScreenCover(isPresented: $showJourney) {
JourneyView(host: host)
}
}
}prepare() is safe to call more than once. Concurrent calls share the same
work, and a fresh preparation returns immediately. JourneyView(host:) calls
present() when it appears and dismiss() when it leaves.
For UIKit, keep the same host in the owning view controller or coordinator:
let host = JourneyHost(configuration: configuration)
Task { try? await host.prepare() }
let journey = JourneyViewController(host: host)
present(journey, animated: true)You can also call try await host.present() yourself when managing the host's
view directly. Observe host.readiness and host.isPresented when native UI
needs to reflect its state.
Prepared and direct presentation
A current renderer advertises preparation and visibility support. The SDK
initializes it while hidden and waits until the first screen's essential layout
and assets have rendered. Hidden preparation does not start presentation
analytics. Showing the Journey reuses that same WKWebView.
An older renderer that does not advertise those capabilities uses direct presentation. The SDK still fetches its configuration ahead of time, but waits to initialize the renderer until the Journey is visible. This avoids recording a hidden visit and keeps older apps working during a renderer rollout.
Each new presentation receives a new client session. After dismissal, a capable renderer resets the flow while hidden so reopening starts at the first screen without allocating another web view.
Loading, errors, and retries
The built-in loading state is a text-free progress indicator with an accessible label. It avoids animation when Reduce Motion is enabled. Presentation has a 15-second deadline; the built-in error state gives the user a generic message and a Try again action. A retry attempts preparation again.
Pass custom SwiftUI views when your app needs its own treatment:
JourneyView(
host: host,
loadingView: AnyView(MyJourneyLoader()),
errorView: { error, retry in
AnyView(MyJourneyError(onRetry: retry))
}
)Avoid showing error.localizedDescription directly in customer-facing UI. It
may contain network or renderer details. Send the error to your logging system
and keep the visible message focused on retrying or leaving the flow.
Freshness and offline starts
FounderHQ returns a refresh interval, which the SDK clamps to 10–30 minutes. While foregrounded, the host schedules one refresh when due, whether prepared or active. A successful refresh during an active Journey stages the next configuration without changing the displayed content or answers.
For the first 10 minutes, a ready preparation opens immediately. Between its refresh time and 30 minutes, it still opens immediately and refreshes asynchronously. Content older than 30 minutes requires a successful fetch before a new presentation. An active Journey is not automatically ended when its original preparation passes that age.
Only successful configuration responses, or authenticated unchanged responses
matching cached JSON, renew freshness. Failures do not renew it or schedule an
automatic retry loop. Presenting again, resuming, or explicitly retrying can
make another attempt after backoff. Requests coalesce and honor Retry-After.
A definitive 401, 403, or confirmed Journey 404 discards prepared content and
stops active interaction. Try again, prepare(), or present() performs
fresh authorization with the same configuration; it never reuses denied
content. Downloaded content can remain visible offline until the device
receives a denial. Restarting the app requires fresh authorization for new
presentations, independently of pending answer delivery.
Identity, authorization, and capture
The publishable key authorizes displaying the Journey. JourneyIdentity
controls who the response is attributed to; it does not grant access. When the
signed-in person, publishable key, Journey ID, base URL, or other configuration
changes, call:
host.updateConfiguration(updatedConfiguration)The host invalidates the prepared content and prepares the new scope. This prevents a Journey prepared for one person from being shown to another.
Capture is enabled by default and is separate from display authorization. Set
capture: nil to disable it. Capture request bodies pass through the native SDK
unchanged. Every queued event keeps the visitor, presentation session, Journey
revision, and context recorded when the event was created. If an answer is sent
later, including after an app restart or identity change, it remains attached
to that original immutable revision and session.
Publishing while a Journey is open does not replace its active content. The next successful preparation can use the new revision; the open presentation and its delayed answers stay tied to the revision that the user saw.
Dispose of a host
Call dispose() when the owning flow is permanently finished:
host.dispose()Disposal cancels preparation and refresh work, removes lifecycle and bridge
handlers, stops the renderer, and makes the host unavailable for future
presentation. Use dismiss() when the Journey may be opened again. Memory
warnings unload hidden renderer content while preserving an active Journey.
An unloaded preparation can rebuild its renderer within the same freshness
window.
Upgrade from an earlier version
Update the Swift Package Manager dependency to 0.8.0 or the CocoaPods entry to:
pod 'FounderHQJourneys', '~> 0.8.0'Existing JourneyView(configuration:...) and JourneyViewController
initializers remain supported. They create and manage a host for that view, so
an existing integration can upgrade without changing its presentation code.
To gain preparation and repeated presentation, move the configuration and
callbacks into a long-lived JourneyHost, call prepare() before the launch
action, and pass the host to JourneyView(host:) or
JourneyViewController(host:). Keep one host per presentation surface and call
updateConfiguration(_:) whenever its identity or Journey scope changes.