Our shared SwiftUI starter: what's in it, what's not
Every app we've shipped at mk0.net forks the same SwiftUI starter. It's about 1,500 lines of plumbing - onboarding, theme, settings, paywall, locale, import/export - and it's the single biggest reason we ship 48 apps in a year. This post breaks down what's in it, what's deliberately excluded, and the discipline that keeps it useful instead of crushing.
Why a template matters
Most indie iOS apps ship the same boilerplate every time: an onboarding flow, a theme system, a settings screen, a paywall, a locale switcher. Reimplementing each of these takes 2-3 weeks per project - pure time tax with no product differentiation.
A shared starter pays back immediately on app two. By app five, you can ship a focused v1 in a weekend. By app fifteen, the template is more battle-tested than anything you'd write from scratch.
What's in it
The non-negotiables we copy into every project:
- Onboarding. A 3-step paged onboarding with fade transitions, app icon hero, value-prop cards, and a primary CTA. Skip-able, returnable from settings.
- Theme system. A small
Themestruct with primary/secondary/accent/background tokens, light + dark variants, and per-app overrides. Wrapped in anEnvironmentValueskey. - Settings. Sectioned list with subscription status, notification toggles, units, language switcher, theme picker, support links, privacy/ToS, version footer.
- Paywall. A RevenueCat-backed paywall with weekly/yearly plans, free trial language localized, restore button, fine print. One file, three layouts you swap by case.
- Locale router. An app-wide locale override that reads from
UserDefaultsand falls back to the system locale. - Import/Export. JSON export + import for the user's data, with a built-in 'erase all data' destructive action.
- Subscription gate. A
@PaidFeatureview modifier that shows the paywall when a non-subscriber taps a gated control. - Crash-safe persistence helper. A wrapper around
ModelContainerthat catches the corruption case and offers a 'reset and re-sync from CloudKit' path.
What's NOT in it
The starter is intentionally narrow. We do not include:
- Per-app data models (every app has its own - that's the actual product)
- Marketing copy, app descriptions, screenshots (we generate these per app via ASO research)
- Networking layers (most of our apps are local-only - no need)
- Authentication (we use iCloud or nothing)
- Analytics SDKs (we ship apps with zero third-party trackers)
- Heavy UI components like calendars or photo galleries - those go in per-app code where they earn their weight
Forking discipline
The discipline that keeps the template useful: when you hit a rough edge in any single app, you fix it in the template, then re-pull into other apps that need it. This requires a small amount of Git pain but compounds enormously.
We treat the template like a library: bug fixes propagate, feature additions are opt-in. We don't use Git submodules - we copy and merge by hand, because the value is in the audit trail of decisions, not the binary inheritance.
Five things that made it 10x better
- Inlining onboarding steps as
some Viewfunctions, not separate ViewControllers. Cuts file count, easier to A/B. - Splitting the paywall layout from the paywall logic. Three layouts share one StoreKit observer.
- A single
AppThemeenvironment that every screen reads. No global singletons. - Ship export/import on day one. Users notice. Reviewers notice. App Review notices.
- A debug menu (only in DEBUG builds) that exports the SwiftData store as JSON. Worth its weight in gold during development.
Why we don't open-source it
We've been asked. The honest reason: the template only works because it's tightly coupled to our taste, our paywall copy, our settings sections. The moment it tries to be generic, it becomes 10x bigger and we'd stop using it.
If you want to build your own: start by shipping two apps without one. The template will write itself.