> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getflowpilot.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom screens

> Replace an entire flow screen with your own native view. SDK registration API, current status, and the editor linkage.

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.

<Warning>
  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 does not yet render it. 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](/ios-sdk/custom-components), which is fully wired. This page documents the SDK surface for completeness and will be updated when the render path and editor authoring land.
</Warning>

## The SDK API

Registration mirrors custom components, keyed by a screen identifier:

```swift theme={null}
public func registerCustomScreen(_ screenId: String, definition: CustomScreenDefinition)
public func unregisterCustomScreen(_ screenId: String)
```

`CustomScreenDefinition` declares inputs, outputs, and the factory:

```swift theme={null}
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:

```swift theme={null}
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:

```swift theme={null}
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)](/editor/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:

```swift theme={null}
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(_:)`.

## Related pages

* [Custom components](/ios-sdk/custom-components)
* [Custom components in the editor](/editor/custom-components)
* [Persistent UI (zones)](/editor/zones)
* [Error handling](/ios-sdk/error-handling)
