Example Features
Todos, Home, Profile, Refer a Friend, and permission priming — the replace-me screens.
These are the app's Example screens — working features wired to real repositories that exist to be studied, copied, and then replaced with your own product. None of them is load-bearing: delete Todos and the app still builds. Each screen is a small, honest demonstration of the patterns documented elsewhere in these docs, so treat them as the reference implementations to copy from.
Example vs Removable demo: the screens here are meant to be replaced (you'll build your own Home, your own first feature). The Showcase tab is meant to be deleted wholesale. Both are non-core.
The main shell has four tabs — Home, Component, Showcase, Me — plus a
side drawer for everything else (Uploads, Refer, Personal Info, Account &
Security, Notifications). Todos, Refer, and Permissions are pushed onto a tab's
NavigationStack, not tabs themselves.
Todos — the reference feature
Features/Todos/ is the one to copy first. It's a full CRUD feature built the
"right way" end-to-end, so it exercises every layer described in
Data Layer: Todo model → TodosRemoteDataSource →
TodosRepository → TodosViewModel → TodosView.
What it demonstrates:
- Full CRUD — add, inline-edit (tap to edit in place, save/cancel), toggle
complete, delete, and reorder (move up/down) against the Supabase
todostable. - Filter chips — All / Active / Completed, with a filter-aware empty state
(
todos.empty/todos.noActive/todos.noCompleted). - Clear-completed confirmation — a destructive bulk action gated behind a confirmation dialog.
- Optimistic updates with rollback — toggle, delete, reorder, and clear-completed all mutate local state immediately, then reconcile with the server; on failure they restore the previous list and surface an inline error. This is the offline/latency story: the UI never blocks on the network.
UiState<[Todo]>— the view switches over.idle/.loading/.data/.errorand renders the feedback trio accordingly, with pull-to-refresh.- Signed-out state — when there's no session, Todos shows a sign-in prompt instead of an empty list (RLS would reject the query anyway).
// Optimistic toggle: flip locally, reconcile with the server, roll back on error.
func toggle(id: UUID) {
guard let existing = todos.first(where: { $0.id == id }) else { return }
var optimistic = existing
optimistic.completed.toggle()
upsert(optimistic)
Task {
await runInlineMutation {
let updated = try await repository.toggle(id: id)
upsert(updated)
} onError: {
self.upsert(existing) // restore on failure
}
}
}Copying Todos into your own feature is the walkthrough in Data Layer → Adding your own feature. Swap the model, the table, and the repository; keep the view-model shape.
Home — the product landing page
Features/Home/HomeView.swift is deliberately not a generic app home — in
the template it's a product pitch for someone evaluating the starter. It's the
clearest screen to gut and rebuild for your own app. It has three parts:
- Hero + stack badges — the product title, value prop, and a wrapping
FlowLayoutof technology badges (SwiftUI, Supabase, RevenueCat…). The badges are intentionally not localized (same reason "Supabase" isn't translated). - State-aware license card — a small, testable state machine
(
HomeLicenseState.from(isConfigured:isSignedIn:isEntitled:)) picks between a "buy" CTA and an "owned" card. It readsenv.entitlements.isEntitled— the sharedsubscriptionsread-model, not the RevenueCat SDK. With no RevenueCat key it falls back to the buy CTA rather than a broken "owned" state. - Quick actions — a grid that jumps to the Component gallery, Todos, and the
Upload sample. Navigation is callback-driven (
onOpenTodos,onOpenShowcase, …) so the shell owns cross-tab jumps and Home stays decoupled from the removable Showcase catalog.
When you replace Home, the pattern worth keeping is callback-driven
navigation: the view takes () -> Void closures instead of reaching into the
shell, which is what lets the Showcase be deleted without touching Home.
Profile — Me, Personal Info, Account & Security
Features/Profile/ is the account hub, reached from the Me tab. It splits
into three screens by concern:
| Screen | What it does |
|---|---|
MeView | The account landing page: avatar + display name (ProfileDisplay), entry points into the sections below, and sign-out. Shows a sign-in CTA when signed out. |
PersonalInfoView | Edit the editable profile fields (display name, etc.) and view read-only account facts; writes through ProfileRepository. |
AccountSecurityView | The sensitive-actions screen — change email (with OTP confirm), change password, toggle the biometric app-lock, sign out other sessions, manage subscription, and delete account. |
Each destructive or credential action routes through
AuthRepository and is gated behind a
confirmation dialog (AccountSecurityViewModel.Confirmation). Delete
account calls the delete-account Edge Function, which removes the user's
rows and storage objects before the auth user — Apple requires in-app account
deletion, so keep this wired (see App Store Release).
Refer a Friend
Features/Refer/ReferView.swift is a minimal share flow: a ShareLink
wrapping an invite URL built by ReferralLinkProvider. The URL is
https://<LINKING_DOMAIN>/ when LINKING_DOMAIN
is set, falling back to soar:/// otherwise — so it degrades gracefully with
no secrets, the same as every other service.
It also ties into the rating trigger: when the share sheet has been opened and
the app returns to .active, it records a referral_shared action via
env.rating.recordAction(...), one of the signals feeding the StoreKit
review prompt.
Permissions priming
Features/Permissions/PermissionsView.swift is a priming screen reached
from Me — it explains the benefit of each permission before the native system
dialog appears, which measurably improves opt-in rates. It covers four
PermissionIds: notifications, photos, camera, and tracking (ATT).
Behavior per card:
- Not yet requested → a button that triggers the native prompt.
- Denied or restricted (
requiresSettings) → the button switches to Open Settings, because iOS won't re-show a dialog the user already dismissed. - Statuses refresh on appear via
PermissionsViewModel.refresh().
ATT (App Tracking Transparency) governs third-party, IDFA-linked tracking — not first-party analytics. PostHog does not depend on ATT consent; that card is reserved for future attribution work. Don't gate analytics behind it.
Replacing these screens
A pragmatic order when you start building your own app:
- Keep Todos as a template, build your first real feature by copying its layer stack, then delete Todos.
- Gut Home — replace the hero, license card, and quick actions with your own landing content; keep the callback-driven navigation shape.
- Keep Profile and Permissions mostly as-is — they're generic account plumbing you'll want regardless (especially account deletion, which Apple requires).
- Delete the Showcase once you no longer need the demo catalog.
