Preview Tools (UI Layer)#
The Preview Tools let you iterate on Theme.plist (iOS) or theme styles in themes.xml / styles.xml (Android) and see the result on themed HAAPI screens without rebuilding the running app. Both platforms ship debug-only preview utilities that integrate with their native IDE tooling — Xcode’s Preview canvas on iOS, Android Studio’s Compose @Preview on Android (complemented by a Previewer Host Activity for production-fidelity validation; see Previewer Host Activity ). Use them as the day-to-day surface for theming work; production validation still happens against the running flow.
For the theming surfaces themselves, see Theming .
Experimental API on both platforms. iOS — HaapiUIPreviewer, HaapiUIPreviewerRepresentable, HaapiUIPreviewerStyleProvider, HaapiUIPreviewerWebAuthnVariant are experimental; minor releases may change them without bumping the major version. Android — all public previewer types are annotated with @ExperimentalHaapiApi and require @OptIn(ExperimentalHaapiApi::class) at use sites. Feedback to Curity is welcomed.
What’s Available#
Both platforms cover the same set of HAAPI screen types and most component galleries. Android additionally ships a Previewer Host Activity that runs the full production Fragment pipeline for theme-fidelity validation.
| iOS UIKit | Android UIWidget | |
|---|---|---|
| IDE integration | Xcode #Preview macro / PreviewProvider | Android Studio Compose @Preview + Previewer Host Activity |
| Build configuration | DEBUG only (#if DEBUG) | src/debug/ source set only |
| Screen previews | 7 (Form, Selector, Polling, BankID, Problem, Generic, WebAuthn) | 7 (same set) |
| Component galleries | 9 (button, message, input, header, checkbox, loading, link, notification banner, expandable) | 10 (button, message, input, checkbox, header, link, loading, spinner-text, expandable, snackbar) ¹ |
| WebAuthn variants | Registration, Authentication, Additional Registration, Platform-Only | Same four variants |
| Production-fidelity validation | n/a — single rendering path | Previewer Host Activity runs real Fragments with real FragmentManager lifecycle |
¹ The platforms name their transient-feedback gallery differently — iOS calls it notificationBannerGallery, Android calls it SnackbarGallery — but they cover the same UI concept (success and error toast-style messages). Android additionally exposes a SpinnerTextViewGallery for the dropdown selector component, which is what brings the count from 9 to 10.
Setup and Usage#
Requirements#
- iOS 14.0+ runtime target
- Xcode 15+ for the
#Previewmacro;PreviewProviderworks on earlier Xcode versions - DEBUG builds only — all preview types are wrapped in
#if DEBUG, so they don’t ship in your release binary
Minimal Preview#
Create a Swift file in your app target (a Previews/ group is a common convention) and add:
import SwiftUI
import IdsvrHaapiUIKit
#Preview("Login Form") {
try! HaapiUIPreviewer.formViewController()
}Open the Xcode canvas (Editor → Canvas or Cmd+Option+Return) and click Resume if needed. A login form renders with the default HAAPI theme.
Using Your Custom Theme#
Pass the plist name (without extension) to render with your app’s theme:
#Preview("Login Form — My Brand") {
try! HaapiUIPreviewer.formViewController(theme: "MyCustomTheme")
}In the preview context, Bundle.main points to Xcode’s preview host rather than your app. If the theme doesn’t appear, pass the bundle explicitly:
#Preview("Login Form — My Brand") {
try! HaapiUIPreviewer.formViewController(
theme: "MyCustomTheme",
bundle: Bundle(for: AppDelegate.self)
)
}Screen-Level Previews#
Seven factory methods cover every prebuilt view controller. Each maps to the matching production class documented in UI Extensibility ’s Default Model → View Mapping table. All accept optional custom HAAPI JSON, a theme name, a bundle, and an embeddedInFlow flag (renders inside a flow container matching production HaapiFlowViewController layout):
formViewController— rendersFormViewControllerselectorViewController— rendersSelectorViewControllerproblemViewController— rendersProblemViewControllerpollingViewController— rendersPollingViewControllerbankIdViewController— rendersBankIdViewControllergenericViewController— rendersGenericViewControllerwebAuthnViewController— rendersWebAuthnViewController; also acceptsvariant: HaapiUIPreviewerWebAuthnVariantto choose between registration, authentication, additional registration, and platform-only (timeout/retry) layouts
Component Galleries#
Nine gallery methods render every style variant of a single component side-by-side:
actionableButtonGallery(Primary, Secondary, Text, LinkView variants)messageViewGallery(Error, Warning, Info, Heading, Content, Username, RecipientOfCommunication, UserCode)inputTextFieldGallery(Curity, Filled, Outlined — each with normal and error states)headerViewGallery(default header view)checkboxViewGallery(unchecked, checked)loadingIndicatorGallery(indeterminate progress + per-button variants)linkViewGallery(link view)notificationBannerGallery(default + success variants — toast-style transient feedback)expandableViewGallery(collapsed, expanded)
Custom HAAPI JSON#
Each factory method accepts a JSON fixture you can supply to render a non-default state — login form with prefilled errors, polling screen mid-countdown, etc.:
#Preview("Login Form — With Error") {
try! HaapiUIPreviewer.formViewController(
haapiJson: MyFixtures.loginWithError,
theme: "MyCustomTheme"
)
}Preview factory methods throw on misconfiguration — bad JSON, missing plist key, malformed style value. Use try! for fast feedback during development; the thrown error message points at the offending key.
Workflow Tips#
- Swift code changes in
#Previewblocks refresh automatically once the canvas resumes. - Theme.plist changes require the canvas to be rebuilt (the Resume button) — Xcode does not re-parse plists on every keystroke.
- Keep the Xcode console open the first time you wire a new theme. Plist parse errors and missing style keys log there with a descriptive message.
- Group preview files under a
Previews/folder per the convention; this also makes them easy to exclude from production-test code-coverage reports. - SwiftUI-only apps without an
AppDelegateclass can passBundle(identifier: "com.example.app")to thebundle:parameter instead ofBundle(for: AppDelegate.self).
Android offers two complementary preview mechanisms. Compose @Preview is for rapid theming iteration in the Android Studio preview pane. The Previewer Host Activity is for production-fidelity validation — it runs the real Fragment pipeline with real FragmentManager lifecycle.
Compose @Preview | Previewer Host Activity | |
|---|---|---|
| Use for | Day-to-day theming work | Final validation before shipping theme changes |
| Feedback speed | Instant on Kotlin changes | Build + install + launch |
| Rendering path | Manual layout inflation, no FragmentManager | Real Fragments with full lifecycle |
| Component galleries | Yes (10 types) | Screen-level only |
Compose @Preview — Setup#
Add the Compose compiler Gradle plugin and Compose dependencies as debugImplementation so they ship only in debug builds:
// app/build.gradle
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.plugin.compose'
}
android {
buildFeatures { compose true }
}
dependencies {
// Compose compiler in release builds (no shipping cost):
compileOnly "androidx.compose.runtime:runtime:1.8.2"
// Compose libraries — debug only:
debugImplementation platform("androidx.compose:compose-bom:2025.05.01")
debugImplementation "androidx.compose.ui:ui"
debugImplementation "androidx.compose.ui:ui-tooling"
debugImplementation "androidx.compose.ui:ui-tooling-preview"
debugImplementation "androidx.compose.material3:material3"
debugImplementation "androidx.compose.foundation:foundation"
}Compose @Preview — Minimal Preview#
Create a file in your app’s src/debug/ source set (e.g., app/src/debug/java/.../previewer/MyPreviews.kt):
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import se.curity.identityserver.haapi.android.ui.widget.ExperimentalHaapiApi
import se.curity.identityserver.haapi.android.ui.widget.previewer.HaapiUIPreviewer
@OptIn(ExperimentalHaapiApi::class)
@Preview(showBackground = true)
@Composable
fun LoginFormPreview() {
HaapiUIPreviewer.FormFragment()
}The preview pane renders a login form with the default Curity theme.
Compose @Preview — Screen Previews#
Seven preview functions cover the HAAPI screens. Each maps to the matching production fragment documented in UI Extensibility ’s Default Model → View Mapping table. All accept json: String? (custom HAAPI JSON; null uses a built-in default), @StyleRes themeResId: Int (theme resource), and showInContainer: Boolean (embed in full HaapiFlowActivity layout):
FormFragment— rendersFormFragmentSelectorFragment— rendersSelectorFragmentPollingFragment— rendersPollingFragmentBankIdFragment— rendersBankIdFragmentProblemFragment— rendersProblemFragmentGenericFragment— rendersGenericFragmentWebAuthnFragment— rendersWebAuthnFragment; also acceptsvariant: HaapiUIPreviewerWebAuthnVariant(REGISTRATION,AUTHENTICATION,PLATFORM_ONLY,ADDITIONAL_REGISTRATION)
Compose @Preview — Component Galleries#
Ten gallery functions render every style variant of one component side-by-side:
ButtonGallery(Primary, Secondary, Text)MessageViewGallery(Error, Warning, Info, Heading, Content, Username, RecipientOfCommunication, UserCode)InputTextFieldGallery(Curity, Filled, Outlined — each with normal and error states)CheckboxGallery,HeaderViewGallery,LinkViewGallery,LoadingIndicatorGallery,SpinnerTextViewGallery,ExpandableInfoViewGallery,SnackbarGallery
Compose @Preview — Custom Theme, Dark Mode, JSON#
Theme via themeResId; dark mode via @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES); custom HAAPI JSON via the json parameter. Custom themes must extend Theme.Haapi.Ui.Widget.BaseTheme:
@Preview(showBackground = true, name = "Form (Dark)",
uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun FormDarkPreview() {
HaapiUIPreviewer.FormFragment(themeResId = R.style.MyCustomHaapiTheme)
}Note: XML resource changes (colors, styles, dimensions, themes) require a project rebuild before previews update — Cmd+Shift+F5 (macOS) or Ctrl+Shift+F5 (Windows/Linux). Kotlin code changes in @Preview functions refresh automatically.
Workflow Tips#
- Kotlin code changes in
@Previewfunctions refresh automatically in the preview pane. - XML resource changes (colors, styles, dimensions, themes) require a project rebuild before previews update — Cmd+Shift+F5 (macOS) or Ctrl+Shift+F5 (Windows/Linux).
- Place all preview files in
src/debug/to keep them out of release builds. - Use
@Preview(name = "…")to label previews clearly; group related previews in the same file for side-by-side comparison. - Run Preview (green play icon next to each
@Previewfunction) deploys the composable to a connected device or emulator; useful for verifying scrolling, ripple effects, and touch feedback that the static pane doesn’t capture.
Previewer Host Activity#
When the visual result in @Preview looks fine but you want to verify the real production code path — real Fragments, real ViewModels, real lifecycle — launch the Previewer Host Activity from your app. See Previewer Host Activity for the full setup, scenario list, and limitations.
Both mechanisms render static UI from JSON fixtures — no network requests, no real authentication, no navigation between screens. Button / link taps either no-op (iOS Xcode preview, Android Compose @Preview) or enter a stuck loading state (Android Previewer Host Activity). Use these for visual validation; for flow-behavior verification, run the real app against a Curity Identity Server.
How to implement this: Theming · UI Extensibility · iOS UIKit · Android UIWidget