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
- iOS 14.0+
- Xcode 15+ (for
#Previewmacro support;PreviewProviderworks in earlier versions) - Debug builds only — all preview types are wrapped in
#if DEBUG
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
#Previewblock orPreviewProvider. 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:
- Go to Editor → Canvas in the menu bar (or press Cmd+Option+Return)
- The canvas opens on the right side of the editor
- 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.mainpoints 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:
- Edit your
Theme.plistand save (Cmd+S) - Wait a moment — Xcode may pick up the change automatically
- If the canvas doesn't refresh, press Cmd+Option+P to force a reload
- 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
formViewControllerpreview and anactionableButtonGallerypreview 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 — Custom JSON, older Xcode, and custom VC subclasses
- Component Galleries — Preview individual UI components side-by-side
- Full-Screen Flow Container — More on
embeddedInFlow - WebAuthn Previews — Layout variants, additional registration, passkeys, and discoverable credentials
- How It Works — What happens behind the scenes when you call a preview method
- Theme Development Workflow — Edit-refresh cycle, diagnostics overlay, live log capture
- API Reference — Complete method signatures and parameter descriptions
- Troubleshooting — Common issues and solutions
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
- MessageView / UserCode: The
userCodevariant renders a recovery-codes grid with a copy button rather than a plain text message. It uses sample recovery codes (["A1B2-C3D4", "E5F6-G7H8", "J9K0-L1M2"]) and the copy button label is resolved from the framework's localization bundle. - InputTextField: Three UIKit implementations (
CurityTextField,FilledTextField,OutlinedTextField) are each shown in normal and error state, for a total of 6 items. Error states show the inline validation message styling. - NotificationBanner: Two style variants are shown — the default banner (with action button) and the success variant (no action button). Each style is resolved from the theme plist.
- ExpandableView: Renders in the collapsed state by default. Tap the component in the live preview canvas to toggle expanded/collapsed. The style is resolved via
registry.styleFor(name: "ExpandableView"), not fromcommonStyle. - LoadingIndicator: A 40pt size constraint is applied automatically.
LoadingIndicatorViewreports zerointrinsicContentSizebefore layout, so the constraint is required for it to be visible in the canvas.
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:
- Scroll view with edge inset padding from your theme's
HaapiFlowViewControllerStyle - Header view showing your logo/branding (from
headerViewStylein your theme) - Background color from the flow style
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 supportembeddedInFlow— 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():
- Your custom
Theme.plistis loaded and merged with the framework defaults - The JSON (either the built-in default or your custom string) is decoded into the correct screen model
- Styles are resolved for the specific view controller type
- The view controller is created with the model and styles applied
- 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:
- Edit your
Theme.plist(or custom theme plist) and save your changes - Saving the plist file will trigger a reload of the preview canvas (the canvas tab will show a loading indicator when reloading)
- Press Cmd+Option+P to force a refresh of the preview canvas (if the previous wait had no effect)
- 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:
- Theme plist discovery and merging
- Font registration and validation
- Style resolution errors or fallbacks
- Data mapping and model 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:
- Open the preview canvas
- Right-click the preview and select Debug Preview (or click the debug icon in the canvas toolbar)
- Breakpoints set in your preview code or framework code will be hit
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:
- Verify your theme plist is in the correct bundle and has the expected name
- Check that the JSON is valid HAAPI format
- Enable
showDiagnostics: trueto see bundle resolution details - Look at the error message displayed in the preview for details
Theme changes not reflected after Cmd+Option+P:
- Clean the build folder (Cmd+Shift+K) and resume the preview
- Ensure your plist is listed under the correct target's "Copy Bundle Resources" build phase
Custom theme looks the same as the default:
- In Xcode previews,
Bundle.mainpoints to the preview host process — not your app bundle. The preview utility automatically searches all loaded app bundles (Bundle.allBundles) and framework bundles (Bundle.allFrameworks) for the theme plist. If the plist still isn't found, pass the bundle explicitly:bundle: Bundle(for: AppDelegate.self)
Gallery renders with wrong colors or fonts:
- This is the same bundle issue described above. Pass your bundle explicitly:
actionableButtonGallery(theme: "MyTheme", bundle: Bundle(for: AppDelegate.self)) - If you edited your theme plist, press Cmd+Option+P to force a preview refresh — plist changes are not detected automatically
- Enable
showDiagnostics: trueto see exactly which bundle and plist path was resolved
Bundle.main doesn't find my theme plist:
- This is expected in preview context.
Bundle.mainpoints to the Xcode preview host process, not your app. The preview utility automatically falls back to searching all loaded bundles for the plist. For best results, pass your app bundle explicitly:bundle: Bundle(for: AppDelegate.self).