TL;DR
Three production-ready SwiftUI onboarding patterns: Carousel (swipeable pages — best for feature-rich apps), Highlights (single scrollable page — best for utilities), and Minimal (single screen — best for simple apps). All three templates ship with The Swift Kit, ready to customize with one config change. Scroll down for full code and comparison table.
A great SwiftUI onboarding screen template makes the difference between users staying or bouncing. Here is how to build beautiful onboarding flows in SwiftUI — with actual code for three production-ready patterns, plus the psychology behind what works and what does not.
Why Onboarding Matters: The Numbers
Let me give you the hard data. According to research from Localytics and Appsflyer, apps with a well-designed onboarding flow see up to 50% higher Day-7 retention compared to apps that drop users straight into the main UI. That is the difference between 20% of your users still being active after a week versus 30%. At 10,000 downloads, that is 1,000 additional retained users.
Here are more numbers that should motivate you to get onboarding right:
- 25% of users abandon an app after a single use if they do not immediately understand its value
- 71% of users who complete onboarding are still active after 30 days (vs. 42% who skip it)
- 3-4 screens is the sweet spot — completion rates drop 15% for every screen beyond 5
- Users who see onboarding are 2.3x more likely to convert to a paid plan within the first week
For indie iOS developers, building custom onboarding from scratch takes 10-20 hours. A SwiftUI onboarding screen template cuts that to minutes while giving you a proven conversion pattern.
The 3 Onboarding Patterns Explained
There is no single "best" onboarding style. The right choice depends on your app's complexity, your target audience, and what you need users to understand before they start. Here is a deep dive into each pattern.
1. Carousel Onboarding (Swipeable Pages)
The carousel is the most common onboarding pattern in the App Store. Users swipe through 3-5 pages, each highlighting a key feature or benefit. It works because it is familiar — users already know how to interact with it, and the progressive disclosure keeps cognitive load low.
When to use: Apps with 3-5 distinct features or value propositions that benefit from visual explanation. Fitness apps, productivity tools, and photo editors all do well with carousels.
Pros: Familiar pattern, works well with illustrations, easy to track which screen users drop off on, supports both swipe and button navigation.
Cons: Can feel generic if not designed well, users might skip through too quickly, requires 3-5 good illustrations or screenshots.
Here is a complete SwiftUI implementation using TabView:
// CarouselOnboardingView.swift
import SwiftUI
struct OnboardingPage: Identifiable {
let id = UUID()
let imageName: String
let title: String
let description: String
}
struct CarouselOnboardingView: View {
@State private var currentPage = 0
@Binding var hasCompletedOnboarding: Bool
let pages: [OnboardingPage] = [
OnboardingPage(
imageName: "wand.and.stars",
title: "AI-Powered Editing",
description: "Transform your photos with intelligent filters that adapt to your style."
),
OnboardingPage(
imageName: "icloud.and.arrow.up",
title: "Sync Everywhere",
description: "Your library syncs across all your devices automatically."
),
OnboardingPage(
imageName: "lock.shield",
title: "Private & Secure",
description: "Your photos are encrypted end-to-end. Only you can see them."
),
]
var body: some View {
VStack(spacing: 0) {
// Skip button
HStack {
Spacer()
if currentPage < pages.count - 1 {
Button("Skip") {
hasCompletedOnboarding = true
}
.foregroundStyle(.secondary)
.padding()
}
}
// Pages
TabView(selection: $currentPage) {
ForEach(Array(pages.enumerated()), id: \.element.id) { index, page in
VStack(spacing: 24) {
Spacer()
Image(systemName: page.imageName)
.font(.system(size: 80))
.foregroundStyle(.accent)
Text(page.title)
.font(.title.bold())
Text(page.description)
.font(.body)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 40)
Spacer()
}
.tag(index)
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.animation(.easeInOut, value: currentPage)
// Page indicators
HStack(spacing: 8) {
ForEach(0..<pages.count, id: \.self) { index in
Circle()
.fill(index == currentPage ? Color.accentColor : Color.secondary.opacity(0.3))
.frame(width: index == currentPage ? 10 : 8,
height: index == currentPage ? 10 : 8)
.animation(.spring(response: 0.3), value: currentPage)
}
}
.padding(.bottom, 32)
// Action button
Button {
if currentPage < pages.count - 1 {
withAnimation { currentPage += 1 }
} else {
hasCompletedOnboarding = true
}
} label: {
Text(currentPage < pages.count - 1 ? "Next" : "Get Started")
.font(.headline)
.frame(maxWidth: .infinity)
.padding()
.background(Color.accentColor)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: 14))
}
.padding(.horizontal, 24)
.padding(.bottom, 40)
}
}
}2. Highlights Onboarding (Feature Cards)
The highlights pattern presents all your key features on a single scrollable screen using bold feature cards. Instead of swiping between pages, users scroll through a list of benefits. This works well when your features are best explained with icons and short descriptions rather than full-page illustrations.
When to use: Apps with many small features that do not need individual screens, developer tools, utility apps, or apps where you want to show social proof alongside features.
Pros: Users see everything at once (no hidden content behind swipes), feels modern and clean, easy to add or remove features without redesigning, scrollable so it works with any number of items.
Cons: Less visual impact than carousel, users might scroll past important features, no built-in progress indicator.
// HighlightsOnboardingView.swift
import SwiftUI
struct FeatureHighlight: Identifiable {
let id = UUID()
let icon: String
let title: String
let description: String
let color: Color
}
struct HighlightsOnboardingView: View {
@Binding var hasCompletedOnboarding: Bool
let features: [FeatureHighlight] = [
FeatureHighlight(icon: "bolt.fill", title: "Lightning Fast", description: "Processes your data in under 100ms, even with large datasets.", color: .yellow),
FeatureHighlight(icon: "lock.fill", title: "End-to-End Encrypted", description: "Your data never leaves your device unencrypted.", color: .blue),
FeatureHighlight(icon: "arrow.triangle.2.circlepath", title: "Automatic Sync", description: "Changes sync across all your devices in real time.", color: .green),
FeatureHighlight(icon: "paintbrush.fill", title: "Fully Customizable", description: "Themes, layouts, and workflows that adapt to you.", color: .purple),
FeatureHighlight(icon: "chart.bar.fill", title: "Smart Analytics", description: "Track your progress with beautiful, actionable insights.", color: .orange),
]
var body: some View {
VStack(spacing: 0) {
ScrollView {
VStack(spacing: 32) {
// Header
VStack(spacing: 12) {
Text("Welcome to MyApp")
.font(.largeTitle.bold())
Text("Everything you need, nothing you don't.")
.font(.subheadline)
.foregroundStyle(.secondary)
}
.padding(.top, 60)
// Feature rows
VStack(spacing: 20) {
ForEach(features) { feature in
HStack(spacing: 16) {
Image(systemName: feature.icon)
.font(.title2)
.foregroundStyle(feature.color)
.frame(width: 44, height: 44)
.background(feature.color.opacity(0.15))
.clipShape(RoundedRectangle(cornerRadius: 10))
VStack(alignment: .leading, spacing: 4) {
Text(feature.title)
.font(.headline)
Text(feature.description)
.font(.subheadline)
.foregroundStyle(.secondary)
}
Spacer()
}
.padding(.horizontal, 24)
}
}
}
}
// CTA button
Button {
hasCompletedOnboarding = true
} label: {
Text("Get Started")
.font(.headline)
.frame(maxWidth: .infinity)
.padding()
.background(Color.accentColor)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: 14))
}
.padding(.horizontal, 24)
.padding(.bottom, 40)
}
}
}3. Minimal Onboarding (Single Screen)
The minimal pattern is a single screen with your app's core value proposition, a brief feature list, and a prominent call-to-action button. No swiping, no scrolling, no friction. Users read one screen and they are in. This is increasingly popular with apps that have a clear, simple purpose.
When to use: Simple utility apps, apps where the UI is self-explanatory, apps targeting power users who hate onboarding, or when you want to get users to the core experience as fast as possible.
Pros: Zero friction, highest completion rate (nearly 100% since there is only one screen), fast to implement, works great for A/B testing different value propositions.
Cons: Cannot convey complex features, no progressive disclosure, may not be enough for users who need guidance.
// MinimalOnboardingView.swift
import SwiftUI
struct MinimalOnboardingView: View {
@Binding var hasCompletedOnboarding: Bool
var body: some View {
VStack(spacing: 40) {
Spacer()
// App icon or hero image
Image(systemName: "sparkles")
.font(.system(size: 64))
.foregroundStyle(.accent)
// Value proposition
VStack(spacing: 12) {
Text("Your photos, perfected.")
.font(.largeTitle.bold())
.multilineTextAlignment(.center)
Text("AI-powered editing that learns your style. No subscriptions, no ads, no nonsense.")
.font(.body)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 32)
}
// Quick feature bullets
VStack(alignment: .leading, spacing: 12) {
FeatureBullet(icon: "checkmark.circle.fill", text: "One-tap enhancement")
FeatureBullet(icon: "checkmark.circle.fill", text: "Works offline")
FeatureBullet(icon: "checkmark.circle.fill", text: "No account required")
}
Spacer()
// CTA
Button {
hasCompletedOnboarding = true
} label: {
Text("Start Editing")
.font(.headline)
.frame(maxWidth: .infinity)
.padding()
.background(Color.accentColor)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: 14))
}
.padding(.horizontal, 24)
.padding(.bottom, 40)
}
}
}
struct FeatureBullet: View {
let icon: String
let text: String
var body: some View {
HStack(spacing: 10) {
Image(systemName: icon)
.foregroundStyle(.green)
Text(text)
.font(.subheadline)
}
}
}Comparing the 3 Onboarding Styles
| Criteria | Carousel | Highlights | Minimal |
|---|---|---|---|
| Best for | Feature-rich apps | Multi-feature utilities | Simple, focused apps |
| Number of screens | 3-5 | 1 (scrollable) | 1 |
| Implementation time | 4-8 hours | 2-4 hours | 1-2 hours |
| Completion rate | 65-80% | 85-95% | 95-100% |
| Feature explanation depth | High | Medium | Low |
| User type | General consumers | Mixed audiences | Power users, tech-savvy |
| Illustration required | Yes (per page) | Icons only | Optional |
Onboarding Psychology: What to Show and What NOT to Show
Onboarding is not just about pretty screens. It is about psychological framing — shaping the user's perception of your app in the first 30 seconds. Get this wrong and no amount of design polish will save you.
DO These Things
- Show value, not features — "Save 2 hours every week" is better than "Automatic task scheduling." Users care about outcomes, not mechanisms.
- Keep it to 3-4 screens maximum — Every additional screen after 4 reduces completion rate by roughly 15%. If you cannot explain your app's value in 4 screens, you have a product problem, not an onboarding problem.
- Show social proof — "Trusted by 50,000 developers" or "4.8 stars on the App Store" on the final screen right before the CTA. This reduces hesitation at the decision point.
- Use progressive disclosure — Reveal complexity gradually. Show the 3 most important things during onboarding, and let users discover the rest organically.
- End with a clear action — The final screen should have one prominent button. Not two options, not three. One.
DO NOT Do These Things
- Do not ask for permissions upfront — Requesting notification permission or camera access during onboarding (before the user understands why) results in 40-60% denial rates. Ask in context, when the user is about to use the feature.
- Do not show pricing during onboarding — Unless your entire app is a subscription pitch. Showing prices before value is established triggers loss aversion and increases bounce rates.
- Do not require sign-up before showing value — Forcing account creation before the user has experienced the app is the fastest way to lose them. Let them explore first, then prompt for sign-up when they try to save or sync.
- Do not autoplay videos — Videos increase load time, drain battery, and many users have their phone on silent. Use static illustrations or subtle animations instead.
- Do not use jargon — "Leverage our ML-powered NLP engine" means nothing to 99% of users. "Understands what you type" does.
Onboarding Best Practices Checklist
| Item | Why It Matters | Priority |
|---|---|---|
| Include a Skip button | Returning users and impatient users need an escape hatch | Critical |
| Show progress (dots or bar) | Users need to know how many screens remain | Critical |
| Support both swipe and button | Some users prefer swiping, others prefer tapping Next | High |
| Only show on first launch | Showing onboarding every time is annoying and wastes time | Critical |
| Test on smallest screen | If it works on iPhone SE, it works everywhere | High |
| Test in both light and dark mode | Illustrations and colors must look good in both | High |
| Use Dynamic Type | Users with accessibility needs must be able to read your text | High |
| Support VoiceOver | Required for accessibility and App Store compliance | High |
| Load instantly (no spinners) | First impression must be instant, not a loading screen | Critical |
| Track completion analytics | You cannot improve what you do not measure | Medium |
| Support landscape (iPad) | If your app supports iPad, onboarding must too | Medium |
Customizing Onboarding Content
The code examples above use an OnboardingPage model to define content. This makes it trivial to swap out pages, reorder them, or A/B test different content without changing any view code:
// OnboardingConfig.swift
struct OnboardingConfig {
let pages: [OnboardingPage]
let ctaText: String
let skipText: String
let showProgressDots: Bool
static let standard = OnboardingConfig(
pages: [
OnboardingPage(imageName: "wand.and.stars", title: "AI Editing", description: "One-tap enhancements powered by machine learning."),
OnboardingPage(imageName: "icloud", title: "Cloud Sync", description: "Your work follows you across all devices."),
OnboardingPage(imageName: "lock.shield", title: "Private", description: "Encrypted end-to-end. We never see your data."),
],
ctaText: "Get Started",
skipText: "Skip",
showProgressDots: true
)
static let minimal = OnboardingConfig(
pages: [
OnboardingPage(imageName: "sparkles", title: "Welcome", description: "The fastest way to edit photos on iOS."),
],
ctaText: "Start Editing",
skipText: "",
showProgressDots: false
)
}Conditionally Show Onboarding Only on First Launch
This is critical — you should only show onboarding once. Use @AppStorage to persist a boolean flag that survives app restarts:
// ContentView.swift
import SwiftUI
struct ContentView: View {
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = false
var body: some View {
if hasCompletedOnboarding {
MainTabView()
} else {
CarouselOnboardingView(hasCompletedOnboarding: $hasCompletedOnboarding)
}
}
}
// If you want to let users replay onboarding from Settings:
struct SettingsView: View {
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = false
var body: some View {
Button("Replay Onboarding") {
hasCompletedOnboarding = false
}
}
}@AppStorage writes to UserDefaults under the hood. The value persists across app launches but is cleared if the user deletes and reinstalls the app — which is actually what you want, since a fresh install should show onboarding again.
Dark Mode Considerations for Onboarding
If your onboarding uses custom illustrations or images, you need to provide variants for both light and dark mode. Here is how I handle this:
- SF Symbols — They adapt to color scheme automatically. If you are using SF Symbols for onboarding icons (as in the code examples above), you get dark mode for free.
- Custom illustrations — In your asset catalog, add both "Any Appearance" and "Dark" variants for each image. SwiftUI's
Imageview will automatically pick the right one. - Background colors — Use semantic colors like
.backgroundand.secondaryBackgroundinstead of hardcoded values. Or use your app's design tokens. - Text colors — Use
.primaryand.secondaryforeground styles. Never hardcode.whiteor.blackfor text on onboarding screens. - Test both modes — In Xcode, use the Environment Overrides panel (Debug → View Debugging → Environment Overrides) to toggle between light and dark mode while the simulator is running.
Accessibility in Onboarding
Accessibility is not optional. Beyond being the right thing to do, Apple has increasingly flagged apps with poor accessibility during review. Here is what you need to handle:
- VoiceOver — Every onboarding element needs a meaningful
accessibilityLabel. The page indicator dots should announce "Page 2 of 4." The Skip button should be reachable via VoiceOver navigation. - Dynamic Type — Use SwiftUI's built-in text styles (
.title,.body,.caption) instead of fixed font sizes. Test with the largest accessibility text size to make sure nothing clips or overlaps. - Reduce Motion — If your onboarding includes animations, respect the
accessibilityReduceMotionpreference. Replace spring animations with simple fades or remove them entirely. - Color contrast — Ensure text meets WCAG 2.1 AA contrast ratios (4.5:1 for body text, 3:1 for large text). Use Xcode's Accessibility Inspector to verify.
// Respecting Reduce Motion
struct AnimatedOnboardingView: View {
@Environment(\.accessibilityReduceMotion) var reduceMotion
var body: some View {
Image(systemName: "sparkles")
.transition(reduceMotion ? .opacity : .scale.combined(with: .opacity))
.animation(reduceMotion ? .none : .spring(response: 0.6), value: currentPage)
}
}Analytics: What to Track During Onboarding
You cannot improve onboarding without data. Here are the specific events I track in every app:
- Onboarding started — Fires when the onboarding view appears. Lets you calculate what percentage of installs actually see onboarding (vs. crashing on launch or other issues).
- Page viewed — Fires when each page becomes visible, with the page index. This shows you exactly where users drop off. If 80% of users leave on page 3, that page needs work.
- Skip tapped — Fires when the user taps Skip, with the current page index. High skip rates on page 1 mean your first screen is not compelling enough.
- Onboarding completed — Fires when the user taps the final CTA. Your completion rate (completed / started) should be above 70%. Below that, something is wrong.
- Time spent per page — Calculate the duration between page view events. If users spend less than 2 seconds on a page, they are not reading it. If they spend more than 10 seconds, the content might be confusing.
Track these with whatever analytics tool you use — Firebase Analytics, Mixpanel, PostHog, or even a simple Supabase table where you insert events.
A/B Testing Onboarding Flows
Once you have analytics in place, A/B testing becomes straightforward. The simplest approach: use a remote config (Firebase Remote Config, RevenueCat Offerings, or a Supabase table) to decide which onboarding variant to show. Here is the pattern:
// OnboardingABTest.swift
enum OnboardingVariant: String {
case carousel = "carousel"
case highlights = "highlights"
case minimal = "minimal"
}
struct OnboardingRouter: View {
@Binding var hasCompletedOnboarding: Bool
let variant: OnboardingVariant
var body: some View {
switch variant {
case .carousel:
CarouselOnboardingView(hasCompletedOnboarding: $hasCompletedOnboarding)
case .highlights:
HighlightsOnboardingView(hasCompletedOnboarding: $hasCompletedOnboarding)
case .minimal:
MinimalOnboardingView(hasCompletedOnboarding: $hasCompletedOnboarding)
}
}
}Run each variant for at least 1,000 users before drawing conclusions. Track the completion rate and, more importantly, the Day-7 retention rate for each group. The variant with the highest retention wins, even if it has a lower completion rate — because retention is what actually matters.
Common Onboarding Mistakes
I have reviewed dozens of indie iOS apps, and these mistakes come up over and over:
- Too many screens — I have seen onboarding flows with 8 or 9 pages. Completion rates were under 30%. Cut it to 3-4 and watch completion double.
- Asking for too much information — Name, email, birthday, preferences, notification permission, camera access — all before the user has seen the app. Each ask is a chance for the user to leave. Defer everything that is not essential.
- No skip option — Some developers worry that users will miss important information if they can skip. In practice, users who want to skip will just force-quit the app instead. Give them the option and they are more likely to stay.
- Showing onboarding after every update — Unless you have a major new feature that fundamentally changes how the app works, do not re-trigger onboarding. Use a "What's New" sheet instead.
- Identical screens with different text — If every page looks the same except for the copy, users stop paying attention by page 2. Make each screen visually distinct.
- Forgetting iPad and landscape — If your app runs on iPad, test onboarding in both portrait and landscape. A carousel that looks great on iPhone can break badly on a 12.9-inch screen.
More SwiftUI Screen Templates
If you liked this onboarding breakdown, explore our other SwiftUI screen template guides:
- SwiftUI Paywall Design Best Practices — high-converting subscription screens
- Sign in with Apple SwiftUI Tutorial — production-ready auth screens
- SwiftUI Navigation Patterns Guide — tabs, stacks, and deep linking
- Dark Mode Design Tokens Guide — make every screen look great in both modes
Get All 3 Templates in The Swift Kit
All three SwiftUI onboarding screen templates — Carousel, Highlights, and Minimal — are included in The Swift Kit, fully built with dark mode support, accessibility, animation, @AppStorage persistence, and customizable design tokens. Switch between onboarding styles with a single line in AppConfig.swift — no need to rewrite any view code.
Along with onboarding, you also get three paywall templates, Supabase auth + database, AI features (ChatGPT, DALL-E, Vision), analytics integration, and everything else you need to launch your iOS app in 30 days. One-time purchase, lifetime updates. Get The Swift Kit →