NewAppLander — App landing pages in 60s$69$39
The Swift Kit logoThe Swift Kit
Template

SwiftUI Onboarding Screen Template: 3 Production-Ready Designs

Build beautiful onboarding flows with three ready-to-use SwiftUI onboarding templates.

Ahmed GaganAhmed Gagan
6 min read

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

CriteriaCarouselHighlightsMinimal
Best forFeature-rich appsMulti-feature utilitiesSimple, focused apps
Number of screens3-51 (scrollable)1
Implementation time4-8 hours2-4 hours1-2 hours
Completion rate65-80%85-95%95-100%
Feature explanation depthHighMediumLow
User typeGeneral consumersMixed audiencesPower users, tech-savvy
Illustration requiredYes (per page)Icons onlyOptional

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

ItemWhy It MattersPriority
Include a Skip buttonReturning users and impatient users need an escape hatchCritical
Show progress (dots or bar)Users need to know how many screens remainCritical
Support both swipe and buttonSome users prefer swiping, others prefer tapping NextHigh
Only show on first launchShowing onboarding every time is annoying and wastes timeCritical
Test on smallest screenIf it works on iPhone SE, it works everywhereHigh
Test in both light and dark modeIllustrations and colors must look good in bothHigh
Use Dynamic TypeUsers with accessibility needs must be able to read your textHigh
Support VoiceOverRequired for accessibility and App Store complianceHigh
Load instantly (no spinners)First impression must be instant, not a loading screenCritical
Track completion analyticsYou cannot improve what you do not measureMedium
Support landscape (iPad)If your app supports iPad, onboarding must tooMedium

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 Image view will automatically pick the right one.
  • Background colors — Use semantic colors like .background and .secondaryBackground instead of hardcoded values. Or use your app's design tokens.
  • Text colors — Use .primary and .secondary foreground styles. Never hardcode .white or .black for 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 accessibilityReduceMotion preference. 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:

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 →

Share this article

Ready to ship your iOS app faster?

The Swift Kit gives you a production-ready SwiftUI codebase with onboarding, paywalls, auth, AI integrations, and more. Stop building boilerplate. Start building your product.

Get The Swift Kit