Skip to main content
A custom screen replaces a whole screen in a flow with your own native SwiftUI view, while the screen still lives inside the flow’s navigation. Where a custom component is one element on a screen, a custom screen is the entire screen. The intended use is a fully native step such as a paywall or a permissions prompt that still emits events to advance the flow.
Status: the registration API on this page is real and public, but in the current SDK build custom screens are not yet rendered. The SDK stores the registered definition but the rendering pipeline does not look it up or invoke the screen factory (no getCustomScreen lookup exists, and CustomScreenContext / CustomScreenParams are never constructed). There is also no editor authoring path: the editor schema has no custom-screen concept, so flows cannot reference a custom screen today.Treat this as a roadmap API. For native UI inside a flow now, use a custom component, which is fully wired. This page documents the SDK surface for completeness and will be updated when the render path and editor authoring land.TODO: confirm with the team how custom screens will be referenced from the editor and when the SDK render path ships.

The SDK API

Registration mirrors custom components, keyed by a screen identifier:
public func registerCustomScreen(_ screenId: String, definition: CustomScreenDefinition)
public func unregisterCustomScreen(_ screenId: String)
CustomScreenDefinition declares inputs, outputs, and the factory:
public init(
    inputs: [String: VariableType]? = nil,
    outputs: [String: OutputSchema]? = nil,
    factory: @escaping @Sendable @MainActor (CustomScreenParams, CustomScreenContext) -> AnyView
)

Reading inputs

CustomScreenParams exposes the resolved inputs with the same typed accessors as a component:
func string(_ key: String) -> String?
func string(_ key: String, default: String) -> String
func number(_ key: String) -> Double?
func number(_ key: String, default: Double) -> Double
func bool(_ key: String) -> Bool?
func bool(_ key: String, default: Bool) -> Bool

The screen context

CustomScreenContext lets the screen emit events and control the persistent zones drawn over it:
func emit(_ eventName: String, payload: [String: Any]? = nil)
func emit(_ eventName: String)
func setZonesVisible(_ visible: Bool)
func setChromeVisible(_ visible: Bool)   // deprecated, calls setZonesVisible
  • emit works like a custom component’s emit: the named event enters the flow’s interaction system for the flow to act on. The name should match a declared output.
  • setZonesVisible(_:) shows or hides the flow’s persistent UI (the nav bar, footer, and overlay zones) over your native screen. See Persistent UI (zones).
  • setChromeVisible(_:) is deprecated and simply forwards to setZonesVisible(_:). Use setZonesVisible(_:).

Example

How a custom screen would be registered once the render path is active. A native paywall that emits purchased to advance the flow:
import SwiftUI
import FlowPilotSDK

let paywall = CustomScreenDefinition(
    inputs: ["plan": .string],
    outputs: [
        "purchased": OutputSchema(description: "User completed the purchase")
    ],
    factory: { params, context in
        let plan = params.string("plan", default: "pro_monthly")

        return AnyView(
            MyPaywallView(plan: plan) {
                // On a successful purchase, advance the flow.
                context.emit("purchased")
            }
        )
    }
)

FlowPilot.shared?.registerCustomScreen("paywall", definition: paywall)
In the editor (when supported), you would wire the purchased event to a goNext action so the flow moves to the next screen after the purchase.

Common mistakes

  • Expecting it to render today. It will not. The registration succeeds but the SDK does not yet draw custom screens. Use a custom component instead.
  • Same contract pitfalls as components. When this ships, the screen identifier, input keys, and event keys must match between the editor and the SDK.
  • Forgetting to advance the flow. A custom screen that never emits an event leaves the user stuck on a native screen with no way forward. Always emit an event that a flow action can act on.
  • Using setChromeVisible. It is deprecated. Call setZonesVisible(_:).