UI Components

The Core/UI building blocks, toasts, and haptics.

Core/UI/ is the app's component library — branded, reusable SwiftUI views that every feature composes from, so screens stay consistent and a restyle happens in one place. This page inventories them, points you at the live gallery that renders each one, and covers the two app-wide feedback systems (toasts and haptics). All of these consume the theme tokens.

The fastest way to see everything is the Component tab in the running app: ComponentGalleryView renders straight from ComponentCatalog, and tapping a row pushes that component's interactive demo (ComponentDetailView). It's the reference implementation — and, unlike the rest of the app, it's English-only by design (copy is plain strings in the catalog, not Localizable.xcstrings).

The gallery works fully offline and needs no secrets — it's part of the "component gallery tab works offline" install check in Installation.

Component inventory

Grouped the same way the gallery is (ComponentGroup):

Controls

ComponentWhat it is
AppButtonBranded button, 4 variants (primary / secondary / destructive / text), with isLoading / isDisabled / optional icon / fullWidth.
ThemeToggleSegmented Picker binding to ThemeController (system / light / dark).

Inputs

ComponentWhat it is
AppTextFieldLabeled input with leading icon, secure entry, inline error, focus control. Does not own its value — bind a @State string.
PasswordStrengthBarThree-segment meter driven purely by the password string; hides when empty.
OTPInputViewSix-box one-time-code entry; filters non-digits, autofills via .oneTimeCode, fires onComplete on the sixth digit.

Containers

ComponentWhat it is
AppCardGrouped-surface card (appGroupedSurface, rounded corners) wrapping arbitrary content.
SectionHeaderUppercase eyebrow above grouped sections, iOS Settings–style.
FlowLayoutA Layout that wraps chips/badges to new lines instead of scrolling off-screen.

Feedback: the UiState trio

These three render the branches of a view model's UiState<T> (see Architecture):

ComponentShown for
LoadingState.loading — centered spinner + optional caption
ErrorState.error — icon, message, optional Try again retry
EmptyState.data([]) — icon, title, message, optional action

Plus OfflineBanner — a slim top banner wired to NetworkObserver, and ThemeToggle (also a control).

Media

ComponentWhat it is
RemoteImageThin wrapper over Kingfisher's KFImage (memory + disk cache, fade, retry-once). Swap image libraries by editing this one file. See Storage.

Composing a screen

The components snap together predictably — switch over the view model's state and lean on the containers:

struct ExampleView: View {
    @Environment(LocaleController.self) private var locale
    @State private var viewModel: ExampleViewModel

    var body: some View {
        Group {
            switch viewModel.state {
            case .idle, .loading:
                LoadingState(message: locale.t("common.loading"))
            case .error(let message):
                ErrorState(message: message) { Task { await viewModel.load() } }
            case .data(let items) where items.isEmpty:
                EmptyState(icon: "tray", title: locale.t("example.empty"))
            case .data(let items):
                ScrollView {
                    ForEach(items) { item in
                        AppCard { Text(item.title).font(.appHeadline) }
                    }
                }
            }
        }
    }
}

Note the locale.t(...) calls — user-facing copy always goes through the localization helper.

Toasts

ToastController (Core/Toast/) is a @MainActor @Observable mounted once as an overlay in RootView via ToastHost. Fire a transient message from anywhere with the controller off AppEnvironment:

@Environment(AppEnvironment.self) private var env
// …
env.toast.show(locale.t("todos.cleared"), variant: .success)

Three variants (info / success / error) pick the icon and tint; success and error also fire the matching haptic automatically. A toast auto-dismisses after its duration (default 3s), is tap-to-dismiss, and a new one cancels the previous timer.

Haptics

Haptics.shared (Core/Haptics/) is a @MainActor singleton wrapping the UIFeedbackGenerator family behind intent-named methods:

Haptics.shared.selection()   // picker / segmented change
Haptics.shared.light()       // subtle taps
Haptics.shared.medium()
Haptics.shared.heavy()
Haptics.shared.success()     // notification feedback
Haptics.shared.warning()
Haptics.shared.error()

Call Haptics.shared.prepare() just before a moment you know will fire one (e.g. in an .onAppear) to pre-warm the generator and cut first-trigger latency. Toasts already trigger success/error haptics, so you rarely call those directly.

On this page