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 live gallery
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
| Component | What it is |
|---|---|
AppButton | Branded button, 4 variants (primary / secondary / destructive / text), with isLoading / isDisabled / optional icon / fullWidth. |
ThemeToggle | Segmented Picker binding to ThemeController (system / light / dark). |
Inputs
| Component | What it is |
|---|---|
AppTextField | Labeled input with leading icon, secure entry, inline error, focus control. Does not own its value — bind a @State string. |
PasswordStrengthBar | Three-segment meter driven purely by the password string; hides when empty. |
OTPInputView | Six-box one-time-code entry; filters non-digits, autofills via .oneTimeCode, fires onComplete on the sixth digit. |
Containers
| Component | What it is |
|---|---|
AppCard | Grouped-surface card (appGroupedSurface, rounded corners) wrapping arbitrary content. |
SectionHeader | Uppercase eyebrow above grouped sections, iOS Settings–style. |
FlowLayout | A 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):
| Component | Shown 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
| Component | What it is |
|---|---|
RemoteImage | Thin 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.
