HaapiUI Previewer

The HaapiUIPreviewer utility allows you to preview themed HAAPI view controllers directly in the Xcode Preview canvas — without building and running your app.

This dramatically speeds up the theme development workflow: edit your Theme.plist, and see the result instantly in the canvas.

Experimental API: The HaapiUI Previewer (HaapiUIPreviewer, HaapiUIPreviewerRepresentable, HaapiUIPreviewerStyleProvider, HaapiUIPreviewerWebAuthnVariant) is an experimental feature. Its API may receive breaking changes in minor releases without requiring a major version bump. Feedback is welcome.

Requirements

Getting Started

This section walks you through setting up your first preview from scratch. By the end you'll have a live preview of a themed HAAPI screen updating in the Xcode canvas.

Step 1 — Create a preview file

Create a new Swift file in your app target. The name and location don't matter — a common convention is to group previews together:

YourApp/
  Previews/
    HaapiPreviews.swift   ← create this

Tip: Xcode's preview canvas only activates when you have a Swift file open that contains a #Preview block or PreviewProvider. Keep your preview files in a dedicated folder so they're easy to find.

Step 2 — Add your first preview

Open the new file and paste this in:

import SwiftUI
import IdsvrHaapiUIKit

#Preview("Login Form") {
    try! HaapiUIPreviewer.formViewController()
}

That's the minimum. No custom JSON, no theme name — it uses the built-in default fixture and the default Theme.plist bundled with the framework.

Step 3 — Open the Xcode canvas

With HaapiPreviews.swift open in the editor:

  1. Go to Editor → Canvas in the menu bar (or press Cmd+Option+Return)
  2. The canvas opens on the right side of the editor
  3. Click Resume (or press Cmd+Option+P) if Xcode shows a "Paused" state

You should see a login form rendered with the default HAAPI theme.

Step 4 — Switch to your custom theme

If you have a custom Theme.plist in your app bundle, pass its name:

#Preview("Login Form - My Brand") {
    try! HaapiUIPreviewer.formViewController(theme: "MyCustomTheme")
}

The theme: parameter is the filename of your .plist without the extension. The preview utility searches all loaded bundles automatically, so you don't need to specify a bundle for standard app targets.

If the theme doesn't appear: This is a common gotcha. In the preview context, Bundle.main points to Xcode's preview host — not your app. Pass your bundle explicitly:

bundle: Bundle(for: AppDelegate.self)

Step 5 — Add more screens

Preview multiple screens in the same file so you can switch between them in the canvas:

import SwiftUI
import IdsvrHaapiUIKit

#Preview("Login Form") {
    try! HaapiUIPreviewer.formViewController(theme: "MyCustomTheme")
}

#Preview("Authenticator Selection") {
    try! HaapiUIPreviewer.selectorViewController(theme: "MyCustomTheme")
}

#Preview("Error Screen") {
    try! HaapiUIPreviewer.problemViewController(theme: "MyCustomTheme")
}

Each #Preview label appears as a named entry in the canvas. Click between them to check each screen without leaving your editor.

Step 6 — Try the flow container

By default, each VC fills the entire canvas. In your real app, these screens are hosted inside HaapiFlowViewController which adds a scroll view, header, and background. Add embeddedInFlow: true to see the full context:

#Preview("Login Form - In Flow") {
    try! HaapiUIPreviewer.formViewController(
        theme: "MyCustomTheme",
        embeddedInFlow: true
    )
}

Step 7 — Iterate on your theme

Your workflow for theme development:

  1. Edit your Theme.plist and save (Cmd+S)
  2. Wait a moment — Xcode may pick up the change automatically
  3. If the canvas doesn't refresh, press Cmd+Option+P to force a reload
  4. If changes still don't appear, add a harmless edit (e.g. a blank line) to your preview Swift file to trigger a full rebuild

Tip: Keep both a formViewController preview and an actionableButtonGallery preview open at the same time. Galleries show every component variant at once, which makes it much faster to spot the effect of a style change.

Where to go next


Usage Patterns

Available screen types

HaapiUIPreviewer provides factory methods for each view controller type:

Method Screen Type
formViewController() Login / registration forms
selectorViewController() Authenticator selection
problemViewController() Error / problem screens
pollingViewController() Polling / waiting screens
bankIdViewController() BankID authentication
genericViewController() Generic representation
webAuthnViewController() WebAuthn (registration, authentication, additional registration, platform-only)

All methods share the same signature:

public static func formViewController(
    json: String? = nil,              // Optional custom HAAPI JSON
    theme: String = "Theme",          // Theme plist resource name
    bundle: Bundle = .main,           // Bundle containing the theme
    embeddedInFlow: Bool = false,     // Wrap in flow container layout
    showDiagnostics: Bool = false     // Overlay theme resolution diagnostics
) throws -> some View

Using custom JSON

Pass a HAAPI JSON string to preview a specific screen configuration:

#Preview("Registration Form") {
    try! HaapiUIPreviewer.formViewController(
        json: """
        {
            "metadata": {
                "templateArea": "html1",
                "viewName": "authenticator/html-form/create-account/get"
            },
            "type": "registration-step",
            "actions": [{
                "template": "form",
                "kind": "user-register",
                "title": "Create your account",
                "model": {
                    "href": "/dev/authn/register/create/htmlSql",
                    "method": "POST",
                    "type": "application/x-www-form-urlencoded",
                    "actionTitle": "Create Account",
                    "fields": [
                        {"name": "name.givenName", "type": "text", "label": "First name"},
                        {"name": "name.familyName", "type": "text", "label": "Last name"},
                        {"name": "primaryEmail", "type": "text", "label": "E-mail"},
                        {"name": "userName", "type": "username", "label": "Username"},
                        {"name": "password", "type": "password", "label": "Password"},
                        {"name": "agreeToTerms", "type": "checkbox", "label": "I agree to the terms of service", "value": "on"}
                    ]
                }
            }]
        }
        """,
        theme: "MyCustomTheme"
    )
}

Tip: You can capture real HAAPI JSON responses from the Curity Identity Server and paste them directly into your preview to match production screens exactly.

PreviewProvider (iOS 14+ compatibility)

If you need to support Xcode versions prior to the #Preview macro, use PreviewProvider:

@available(iOS 14.0, *)
struct ThemePreviews: PreviewProvider {
    static var previews: some View {
        try! HaapiUIPreviewer.formViewController(theme: "BrandedTheme")
            .previewDisplayName("Branded Login")

        try! HaapiUIPreviewer.selectorViewController()
            .previewDisplayName("Authenticator Selector")

        try! HaapiUIPreviewer.problemViewController()
            .previewDisplayName("Problem")

        try! HaapiUIPreviewer.pollingViewController()
            .previewDisplayName("Polling")
    }
}

Custom view controller subclasses

If you have custom view controllers (either subclassing BaseViewController or conforming to HaapiUIViewController), use HaapiUIPreviewer.loadStyles(...) to get resolved theme styles, and HaapiUIPreviewerRepresentable to wrap your VC for the preview canvas.

Subclassing BaseViewController

#Preview("Subclassed VC") {
    let styles = try! HaapiUIPreviewer.loadStyles()

    return HaapiUIPreviewerRepresentable {
        let model = MyCustomModel(/* ... */)
        let vc = MySubclassedFormVC(
            model,
            style: try styles.formStyle,
            commonStyle: styles.commonStyle
        )
        vc.uiStylableThemeDelegate = styles.themeDelegate
        return vc
    }
}

class MySubclassedFormVC: BaseViewController<MyCustomModel, FormViewControllerStyle> {
    override func viewDidLoad() {
        super.viewDidLoad()
        // Your custom setup
    }
}

Implementing HaapiUIViewController protocol

#Preview("Custom VC") {
    let styles = try! HaapiUIPreviewer.loadStyles()

    return HaapiUIPreviewerRepresentable {
        let model = MyCustomModel(/* ... */)
        let vc = MyCustomVC()
        vc.model = model
        vc.uiStylableThemeDelegate = styles.themeDelegate
        return vc
    }
}

class MyCustomVC: UIViewController, HaapiUIViewController {
    var model: AssociatedModel?
    var haapiFlowViewControllerDelegate: HaapiFlowViewControllerDelegate?
    var uiStylableThemeDelegate: UIStylableThemeDelegate?

    override func viewDidLoad() {
        super.viewDidLoad()
        // Build your custom UI using the resolved model and theme
    }

    func stopLoading() {}
    func hasLoading() -> Bool { false }
    func handleProblemModel(_ problemModel: any ProblemModel) -> Bool { true }
    func handleInfoMessageModels(_ infoMessageModels: [any InfoMessageModel]) {}
    func handleLinkItemModels(_ linkItemModels: [any LinkItemModel]) {}
    func handleFormModel(_ formModel: any FormModel) -> Bool { true }

    func preSubmit(interactionActionModel: any InteractionActionModel,
                   parameters: [String: any Sendable],
                   closure: @escaping (Bool, [String: any Sendable]) -> Void) {
        closure(true, parameters)
    }
    func preSelect(selectorItemModel: any SelectorItemInteractionActionModel,
                   closure: (Bool) -> Void) {
        closure(true)
    }
    func preFollow(linkItemModel: any LinkItemModel, closure: (Bool) -> Void) {
        closure(true)
    }
}

HaapiUIPreviewerStyleProvider reference

HaapiUIPreviewerStyleProvider exposes these properties for resolving styles:

Property Type Notes
commonStyle HaapiUIViewControllerStyle Shared style containing component-level styles
formStyle FormViewControllerStyle Throws if style resolution fails
selectorStyle SelectorViewControllerStyle Throws if style resolution fails
pollingStyle PollingViewControllerStyle Throws if style resolution fails
problemStyle ProblemViewControllerStyle Throws if style resolution fails
bankIdStyle BankIdViewControllerStyle Throws if style resolution fails
genericStyle GenericViewControllerStyle Throws if style resolution fails
webAuthnStyle WebAuthnViewControllerStyle Throws if style resolution fails
flowStyle HaapiFlowViewControllerStyle Throws if style resolution fails
themeDelegate UIStylableThemeDelegate Assign to VC's uiStylableThemeDelegate

Component Galleries

Component galleries let you preview all style variants of a single UI component side-by-side in the Xcode canvas. This is the fastest way to iterate on component-level styling — you can see every variant (e.g. all four button styles) simultaneously without navigating through multiple screens.

Use galleries when you want to tune the look of a specific component. Use the full-screen VC previews (formViewController, bankIdViewController, etc.) when you need to see how a complete screen composes together.

Available gallery methods

Method Variants shown Item count
actionableButtonGallery() Primary, Secondary, Text, Link 4
messageViewGallery() Error, Warn, Info, RecipientOfCommunication, Content, Heading, Username, UserCode 8
inputTextFieldGallery() Curity, Filled, Outlined — each in normal and error state 6
headerViewGallery() Default 1
checkboxViewGallery() Unchecked, Checked 2
loadingIndicatorGallery() Default 1
linkViewGallery() Default 1
notificationBannerGallery() Default, Success 2
expandableViewGallery() Default collapsed (tap in canvas to expand) 1

All gallery methods share the same signature (no json: or embeddedInFlow: parameters — gallery previews don't decode JSON or wrap in a flow container):

public static func actionableButtonGallery(
    theme: String = "Theme",
    bundle: Bundle = .main,
    showDiagnostics: Bool = false
) throws -> some View

Quick start

import IdsvrHaapiUIKit

// See all button variants with the default theme
#Preview("Buttons") {
    try! HaapiUIPreviewer.actionableButtonGallery()
}

// See all message variants
#Preview("Messages") {
    try! HaapiUIPreviewer.messageViewGallery()
}

// See all text field implementations (normal + error states)
#Preview("Text Fields") {
    try! HaapiUIPreviewer.inputTextFieldGallery()
}

// See notification banner styles (Default + Success)
#Preview("Notification Banners") {
    try! HaapiUIPreviewer.notificationBannerGallery()
}

Custom theme

Pass your theme name and the bundle that contains it:

#Preview("Buttons - My Brand") {
    try! HaapiUIPreviewer.actionableButtonGallery(
        theme: "MyCustomTheme",
        bundle: Bundle(for: AppDelegate.self)
    )
}

Diagnostics overlay

Add showDiagnostics: true to any gallery call to show the same bundle/theme resolution overlay as the full-screen VC previews:

#Preview("Buttons - Diagnostics") {
    try! HaapiUIPreviewer.actionableButtonGallery(
        theme: "MyCustomTheme",
        showDiagnostics: true
    )
}

Component-specific notes

Full-Screen Flow Container

By default, preview VCs render in isolation — the view controller content fills the entire preview canvas. In production, however, these VCs are hosted inside HaapiFlowViewController, which provides a scroll view with padding, a header view (logo/branding), and a themed background color.

To see how your themed screens look within the flow container, pass embeddedInFlow: true:

#Preview("Login Form - In Flow") {
    try! HaapiUIPreviewer.formViewController(embeddedInFlow: true)
}

This wraps the view controller in a lightweight container that replicates the visual layout of HaapiFlowViewController:

The container does not include flow logic, navigation, or the close button — it only reproduces the visual structure so you can verify your theme customizations in context.

Comparing isolated vs. flow container previews

Use both side-by-side to iterate efficiently:

// Isolated — focuses on the form content itself
#Preview("Login Form - Isolated") {
    try! HaapiUIPreviewer.formViewController(theme: "MyTheme")
}

// In flow — shows how it looks within the authentication flow
#Preview("Login Form - In Flow") {
    try! HaapiUIPreviewer.formViewController(theme: "MyTheme", embeddedInFlow: true)
}

Combining with other parameters

embeddedInFlow works with all other parameters:

#Preview("Registration - Branded Flow") {
    try! HaapiUIPreviewer.formViewController(
        json: myRegistrationJSON,
        theme: "BrandedTheme",
        bundle: Bundle(for: AppDelegate.self),
        embeddedInFlow: true,
        showDiagnostics: true
    )
}

Note: Gallery previews (actionableButtonGallery(), messageViewGallery(), etc.) do not support embeddedInFlow — they are component-level previews, not screen-level.

WebAuthn Previews

The WebAuthn screen is a special case in the HAAPI flow. Unlike other screens where a single JSON shape produces a single visual layout, the WebAuthn view controller can render three distinct visual layouts depending on the structure of the JSON response from the Curity Identity Server. This section explains each layout variant, what drives the differences, and how to preview them.

Understanding WebAuthn layout variants

The WebAuthnViewController uses an internal state machine that inspects the JSON response to decide which layout to render. There are four built-in visual layouts:

Layout When it appears What you see
Options screen Both platform and cross-platform credentials are available Title, description, and two buttons — one for each authenticator type (e.g. "Use Platform Passkey" and "Use Security Key")
Auto-trigger Only one credential type is available, or the response uses the unified passkey format A loading spinner fills the screen while the WebAuthn ceremony is triggered automatically — no interactive UI
Additional registration The server offers an optional device registration after a successful authentication An informational message, a primary button ("Yes"), and one or more secondary/link buttons ("No, not now", "Don't ask me again")
Platform only (timeout) Only platform credentials available, simulating a ceremony timeout A retry info message and a single platform credential button — what the user sees after the OS credential dialog times out

Variant parameter

The webAuthnViewController method accepts a variant parameter to select which visual layout to preview:

public enum HaapiUIPreviewerWebAuthnVariant {
    case registration              // Options screen (registration flow)
    case authentication            // Options screen (authentication flow)
    case additionalRegistration    // Info message + Yes/No/Don't ask buttons
    case platformOnly              // Retry message + platform button (simulated timeout)
}

The default is .registration. Each variant maps to a built-in JSON fixture — no custom JSON needed:

// Registration: shows platform + cross-platform choice buttons (default)
#Preview("WebAuthn Registration") {
    try! HaapiUIPreviewer.webAuthnViewController()
}

// Authentication: shows platform + cross-platform choice buttons
#Preview("WebAuthn Authentication") {
    try! HaapiUIPreviewer.webAuthnViewController(variant: .authentication)
}

// Additional registration: info message + Yes/No/Don't ask buttons
#Preview("WebAuthn Additional Registration") {
    try! HaapiUIPreviewer.webAuthnViewController(variant: .additionalRegistration)
}

// Platform only: retry message + single platform button (simulated timeout)
#Preview("WebAuthn Platform Only") {
    try! HaapiUIPreviewer.webAuthnViewController(variant: .platformOnly)
}

The default registration JSON includes both platformCredentialCreationOptions and crossPlatformCredentialCreationOptions, which triggers the options screen. The default authentication JSON includes both platformCredentials and crossPlatformCredentials, producing the same options layout. The additional registration JSON includes messages and otherActions, producing the additional registration prompt.

Previewing the additional registration layout

The additional registration screen is an upsell prompt that appears after a user authenticates with a security key. The server asks whether the user wants to also register a built-in authenticator (like Face ID). This layout is unique to WebAuthn — no other HAAPI screen type produces it.

Use the .additionalRegistration variant:

#Preview("WebAuthn Additional Registration") {
    try! HaapiUIPreviewer.webAuthnViewController(variant: .additionalRegistration)
}

The additional registration layout reuses the same button and message component styles as other screens, so any changes you make to actionableButtonStyles or messageViewStyles in your theme plist will be reflected here.

Suppressing the auto-trigger behaviour

In the preview context, the credential API trigger is safely skipped — the view controller displays its content instead of attempting to invoke the underlying OS APIs, which is unsupported in Xcode Previews.

#Preview("WebAuthn Passkey Registration") {
    try! HaapiUIPreviewer.webAuthnViewController(
        json: """
        {
            "metadata": {"viewName": "authenticator/webauthn/register/get"},
            "type": "registration-step",
            "actions": [{
                "template": "client-operation",
                "kind": "device-register",
                "title": "Register new device",
                "model": {
                    "name": "webauthn-registration",
                    "arguments": {
                        "credentialCreationOptions": {
                            "publicKey": {
                                "rp": {"name": "example.com", "id": "example.com"},
                                "user": {"name": "user", "displayName": "user", "id": "abc123"},
                                "challenge": "AAAA",
                                "pubKeyCredParams": [{"alg": -7, "type": "public-key"}],
                                "excludeCredentials": [],
                                "authenticatorSelection": {
                                    "userVerification": "required",
                                    "residentKey": "preferred"
                                },
                                "attestation": "none",
                                "extensions": {}
                            }
                        }
                    },
                    "continueActions": [{
                        "template": "form",
                        "kind": "continue",
                        "title": "Register new device",
                        "model": {
                            "href": "/authn/register/passkeys",
                            "method": "POST",
                            "type": "application/json",
                            "fields": [{"name": "credential", "type": "context"}]
                        }
                    }],
                    "errorActions": [{
                        "template": "form",
                        "kind": "redirect",
                        "model": {
                            "href": "/authn/authenticate/passkeys?_force_external_browser_flow=true",
                            "method": "GET"
                        }
                    }]
                }
            }]
        }
        """
    )
}

Note: For theming work, the Options screen (the default) and Additional registration variants are more productive since they display buttons, messages, and text elements. Tapping credential buttons in the live preview canvas is also safe — the view remains in its current state.

Previewing discoverable credentials (authentication)

Discoverable credentials (conditional UI / passkeys without pre-registered credential IDs) produce a response where both platformCredentials and crossPlatformCredentials are empty arrays. Despite having no specific credentials to list, this still renders the Options screen layout because the SDK treats empty arrays as "both types available":

#Preview("WebAuthn Discoverable Credentials") {
    try! HaapiUIPreviewer.webAuthnViewController(
        variant: .authentication,
        json: """
        {
            "metadata": {"viewName": "authenticator/passkeys/authenticate-device/get"},
            "type": "authentication-step",
            "actions": [{
                "template": "client-operation",
                "kind": "login",
                "title": "Login with passkeys",
                "model": {
                    "name": "webauthn-authentication",
                    "arguments": {
                        "credentialRequestOptions": {
                            "publicKey": {
                                "challenge": "AAAA",
                                "rpId": "example.com",
                                "userVerification": "required",
                                "extensions": {}
                            }
                        },
                        "platformCredentials": [],
                        "crossPlatformCredentials": []
                    },
                    "continueActions": [{
                        "template": "form",
                        "kind": "continue",
                        "title": "Login with passkeys",
                        "model": {
                            "href": "/authn/authenticate/passkeys",
                            "method": "POST",
                            "type": "application/json",
                            "fields": [{"name": "credential", "type": "context"}]
                        }
                    }],
                    "errorActions": [{
                        "template": "form",
                        "kind": "redirect",
                        "model": {
                            "href": "/authn/authenticate/passkeys?_force_external_browser_flow=true",
                            "method": "GET"
                        }
                    }]
                }
            }]
        }
        """
    )
}

Layout variant summary

Variant Preview call JSON key indicators Custom JSON needed?
Options screen (registration) webAuthnViewController() Both platformCredentialCreationOptions + crossPlatformCredentialCreationOptions No — this is the default
Options screen (authentication) webAuthnViewController(variant: .authentication) Both platformCredentials + crossPlatformCredentials (with items) No — built-in variant
Additional registration webAuthnViewController(variant: .additionalRegistration) Has messages + multiple actions (including otherActions) No — built-in variant
Platform only (timeout) webAuthnViewController(variant: .platformOnly) Only platformCredentialCreationOptions (no cross-platform) No — built-in variant
Auto-trigger / loading (registration) webAuthnViewController(json:) Only credentialCreationOptions (unified passkey format) Yes
Auto-trigger / loading (authentication) webAuthnViewController(variant: .authentication, json:) Only one of platformCredentials / crossPlatformCredentials has items Yes
Discoverable credentials webAuthnViewController(variant: .authentication, json:) Both platformCredentials + crossPlatformCredentials are empty [] Yes

WebAuthn and the flow container

WebAuthn previews support embeddedInFlow: true just like other screen types:

#Preview("WebAuthn Registration - In Flow") {
    try! HaapiUIPreviewer.webAuthnViewController(embeddedInFlow: true)
}

A note on credentials API in previews

ASAuthorizationController (the Authentication Services framework) is not available in Xcode Previews. If a credential ceremony were triggered — either automatically (auto-trigger layouts) or by tapping a credential button — it would fail with a .notSupported error, causing the view controller to show an error screen.

To prevent this, the preview utility automatically replaces the credential ceremony with a preview-safe static version. No action is needed on your part — it just works.

How It Works

The preview utility reuses the same pipeline that powers the production HAAPI UI — theme loading, JSON decoding, style resolution, and view controller creation all happen behind the scenes using the framework's real implementation. This means previews render exactly as the production UI would with the same theme and JSON input.

When you call a factory method like HaapiUIPreviewer.formViewController():

  1. Your custom Theme.plist is loaded and merged with the framework defaults
  2. The JSON (either the built-in default or your custom string) is decoded into the correct screen model
  3. Styles are resolved for the specific view controller type
  4. The view controller is created with the model and styles applied
  5. The result is wrapped in a SwiftUI view for display in the Xcode canvas

Gallery previews skip JSON decoding — they construct components directly from the resolved theme styles.

Repeated preview refreshes are efficient because decoded results are cached internally. All preview code is compiled only in debug builds (#if DEBUG) and is never included in release frameworks.

Theme Development Workflow

Depending on Xcode version, the canvas previews may only auto-refresh when Swift source files change. Edits to resource files like .plist may not be detected automatically. Use the following workflow when iterating on theme customizations:

  1. Edit your Theme.plist (or custom theme plist) and save your changes
  2. Saving the plist file will trigger a reload of the preview canvas (the canvas tab will show a loading indicator when reloading)
  3. Press Cmd+Option+P to force a refresh of the preview canvas (if the previous wait had no effect)
  4. The updated theme is applied (canvas reload may take a few seconds to update)

Tip: Making any trivial edit (e.g. adding a space) in the preview Swift file also triggers a full rebuild that picks up plist changes.

Debugging previews

Diagnostic overlay

All HaapiUIPreviewer factory methods — both screen-level and gallery — accept a showDiagnostics parameter. When set to true, the preview renders a semi-transparent overlay at the bottom of the screen showing theme resolution details:

#Preview("Login Form - Diagnostics") {
    try! HaapiUIPreviewer.formViewController(
        theme: "CustomTheme",
        bundle: Bundle(for: AppDelegate.self),
        showDiagnostics: true
    )
}

The overlay displays:

Field Description
Theme The plist resource name being loaded
In preferred bundle Whether the plist was found in the bundle you specified (YES/NO)
Preferred bundle The bundle identifier (or path) you passed
Resolved bundle The bundle where the plist was actually found
Plist path The file path of the resolved plist

If the plist is not found in the preferred bundle, a yellow warning is shown. If it's not found in any bundle, a red error is displayed.

Live log capture

When showDiagnostics is enabled, the overlay also captures and displays HaapiLogger messages in real time. The log collector registers itself as a LogSink and configures logging at debug level for all UIKit follow-up tags, so you see every message produced during theme loading and view controller creation:

Log entries are color-coded by level: red for errors, yellow for warnings, white for info, and gray for debug messages.

This replaces the need for print() statements or a debugger — all diagnostic output is visible directly in the preview canvas.

Xcode debugger (where supported)

In some Xcode versions you can attach the debugger to a running preview:

  1. Open the preview canvas
  2. Right-click the preview and select Debug Preview (or click the debug icon in the canvas toolbar)
  3. Breakpoints set in your preview code or framework code will be hit
  4. print() statements appear in the Xcode debug console

Note: Debug Preview support varies by Xcode version. If the option is not available, use the diagnostic overlay described above.

API Reference

Screen-level preview methods

All seven methods follow the same pattern:

HaapiUIPreviewer.formViewController(json:theme:bundle:embeddedInFlow:showDiagnostics:) throws -> some View
HaapiUIPreviewer.selectorViewController(json:theme:bundle:embeddedInFlow:showDiagnostics:) throws -> some View
HaapiUIPreviewer.problemViewController(json:theme:bundle:embeddedInFlow:showDiagnostics:) throws -> some View
HaapiUIPreviewer.pollingViewController(json:theme:bundle:embeddedInFlow:showDiagnostics:) throws -> some View
HaapiUIPreviewer.bankIdViewController(json:theme:bundle:embeddedInFlow:showDiagnostics:) throws -> some View
HaapiUIPreviewer.genericViewController(json:theme:bundle:embeddedInFlow:showDiagnostics:) throws -> some View
HaapiUIPreviewer.webAuthnViewController(variant:json:theme:bundle:embeddedInFlow:showDiagnostics:) throws -> some View
Parameter Type Default Description
variant HaapiUIPreviewerWebAuthnVariant .registration (WebAuthn only) Selects which visual layout to preview.
json String? nil Custom HAAPI JSON. When nil, a built-in default fixture is used. When provided, overrides variant.
theme String "Theme" Name of the .plist resource containing theme configuration.
bundle Bundle .main Bundle containing the theme plist. Falls back to searching all bundles.
embeddedInFlow Bool false When true, wraps the VC in a flow container with scroll view, padding, header, and background.
showDiagnostics Bool false When true, overlays theme resolution diagnostics and live log capture.

Gallery preview methods

All nine methods follow the same pattern:

HaapiUIPreviewer.actionableButtonGallery(theme:bundle:showDiagnostics:) throws -> some View
HaapiUIPreviewer.messageViewGallery(theme:bundle:showDiagnostics:) throws -> some View
HaapiUIPreviewer.inputTextFieldGallery(theme:bundle:showDiagnostics:) throws -> some View
HaapiUIPreviewer.headerViewGallery(theme:bundle:showDiagnostics:) throws -> some View
HaapiUIPreviewer.checkboxViewGallery(theme:bundle:showDiagnostics:) throws -> some View
HaapiUIPreviewer.loadingIndicatorGallery(theme:bundle:showDiagnostics:) throws -> some View
HaapiUIPreviewer.linkViewGallery(theme:bundle:showDiagnostics:) throws -> some View
HaapiUIPreviewer.notificationBannerGallery(theme:bundle:showDiagnostics:) throws -> some View
HaapiUIPreviewer.expandableViewGallery(theme:bundle:showDiagnostics:) throws -> some View
Parameter Type Default Description
theme String "Theme" Name of the .plist resource containing theme configuration.
bundle Bundle .main Bundle containing the theme plist. Falls back to searching all bundles.
showDiagnostics Bool false When true, overlays theme resolution diagnostics and live log capture.

Style loading for custom VCs

HaapiUIPreviewer.loadStyles(theme:bundle:) throws -> HaapiUIPreviewerStyleProvider

SwiftUI representable

HaapiUIPreviewerRepresentable(_ builder: @escaping @MainActor () throws -> UIViewController)

Wraps any UIKit view controller for the Xcode preview canvas. If the builder throws, an error message is displayed in the preview instead of crashing.

Troubleshooting

Preview shows "Preview Error" message:

Theme changes not reflected after Cmd+Option+P:

Custom theme looks the same as the default:

Gallery renders with wrong colors or fonts:

Bundle.main doesn't find my theme plist: