
Onboarding Generator
- 2 installs
- 591 repo stars
- Updated July 24, 2026
- rshankras/claude-code-apple-skills
Generates a multi-step onboarding flow for iOS/macOS apps with persistence, animations, and accessibility support for the first-launch experience.
About
Generates a customizable multi-step onboarding flow with first-launch persistence, animations, and accessibility support. A developer uses it to add welcome screens or a tutorial to an Apple app.
- Multi-step flow with first-launch persistence
- Animations and accessibility support
Onboarding Generator by the numbers
- 2 all-time installs (skills.sh)
- Ranked #888 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rshankras/claude-code-apple-skills --skill onboarding-generatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 591 |
| Last updated | July 24, 2026 |
| Repository | rshankras/claude-code-apple-skills ↗ |
What it does
Generates a multi-step onboarding flow for iOS/macOS apps with persistence, animations, and accessibility support for the first-launch experience.
Files
Onboarding Generator
Generate a complete, customizable onboarding flow with persistence, animations, and accessibility support.
When This Skill Activates
Use this skill when the user:
- Asks to "add onboarding" or "create onboarding"
- Mentions "welcome screens" or "first launch"
- Wants to "show intro on first launch"
- Asks about "onboarding flow" or "tutorial screens"
Pre-Generation Checks
1. Project Context Detection
- [ ] Check for existing onboarding implementations
- [ ] Identify if SwiftUI or UIKit project
- [ ] Find App entry point location
- [ ] Check deployment target (TabView paging requires iOS 14+)
2. Conflict Detection
Search for existing onboarding:
Glob: **/*Onboarding*.swift, **/*Welcome*.swift
Grep: "hasCompletedOnboarding" or "isFirstLaunch"If found, ask user:
- Replace existing onboarding?
- Keep existing, add new flow?
Configuration Questions
Ask user via AskUserQuestion:
1. Navigation style?
- Paged (horizontal swipe with dots)
- Stepped (Next/Back buttons)
2. Number of screens?
- 2-5 screens (recommend 3-4)
3. Skip option?
- Allow skip button
- Mandatory completion
4. Presentation style?
- Full screen cover (modal)
- Inline (embedded in view hierarchy)
5. Include animations?
- Animated transitions
- Static transitions
Generation Process
Step 1: Create Core Files
Generate these files: 1. OnboardingView.swift - Main container 2. OnboardingPageView.swift - Individual page template 3. OnboardingPage.swift - Page data model 4. OnboardingStorage.swift - Persistence 5. OnboardingModifier.swift - View modifier for easy integration
Step 2: Customize Based on Configuration
Paged Navigation:
TabView(selection: $currentPage) {
ForEach(pages) { page in
OnboardingPageView(page: page)
.tag(page.id)
}
}
.tabViewStyle(.page(indexDisplayMode: .always))Stepped Navigation:
VStack {
OnboardingPageView(page: pages[currentPage])
HStack {
if currentPage > 0 {
Button("Back") { currentPage -= 1 }
}
Spacer()
Button(isLastPage ? "Get Started" : "Next") {
if isLastPage {
completeOnboarding()
} else {
currentPage += 1
}
}
}
}Step 3: Determine File Location
Check project structure:
- If
Sources/exists →Sources/Onboarding/ - If
App/exists →App/Onboarding/ - Otherwise →
Onboarding/
Output Format
After generation, provide:
Files Created
Sources/Onboarding/
├── OnboardingView.swift # Main container
├── OnboardingPageView.swift # Page template
├── OnboardingPage.swift # Data model
├── OnboardingStorage.swift # @AppStorage persistence
└── OnboardingModifier.swift # .onboarding() modifierIntegration Steps
Option 1: View Modifier (Recommended)
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onboarding() // Automatically shows on first launch
}
}
}Option 2: Manual Control
@main
struct MyApp: App {
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = false
var body: some Scene {
WindowGroup {
ContentView()
.fullScreenCover(isPresented: .constant(!hasCompletedOnboarding)) {
OnboardingView()
}
}
}
}Customization
Add Your Content:
// In OnboardingStorage.swift or OnboardingView.swift
static let pages: [OnboardingPage] = [
OnboardingPage(
title: "Welcome",
description: "Your app description here",
imageName: "hand.wave", // SF Symbol or asset name
accentColor: .blue
),
// Add more pages...
]Testing Instructions
1. Delete app from simulator (to reset UserDefaults) 2. Run app - onboarding should appear 3. Complete onboarding 4. Relaunch app - onboarding should NOT appear 5. Reset via Settings or delete app to test again
Debug/Testing Reset
// Add to Settings or debug menu
Button("Reset Onboarding") {
UserDefaults.standard.removeObject(forKey: "hasCompletedOnboarding")
}References
- onboarding-patterns.md - Best practices and design patterns
- templates/ - All template files
Onboarding Patterns and Best Practices
Design Principles
Keep It Short
- 3-4 screens optimal
- Users want to use the app, not read tutorials
- Save detailed tutorials for in-app help
Focus on Value
- Explain benefits, not features
- "Save time" not "Has a calendar sync feature"
- Show outcomes, not mechanics
Make It Skippable
- Power users don't need handholding
- Always provide a way out
- Remember: they already downloaded your app
Navigation Patterns
Paged (Swipe)
Best for: Visual, image-heavy onboarding
TabView(selection: $currentPage) {
ForEach(Array(pages.enumerated()), id: \.element.id) { index, page in
OnboardingPageView(page: page)
.tag(index)
}
}
.tabViewStyle(.page(indexDisplayMode: .always))
.indexViewStyle(.page(backgroundDisplayMode: .always))Pros:
- Intuitive gesture
- Progress visible via dots
- Works well on all screen sizes
Cons:
- Easy to accidentally swipe past content
- Less control over pacing
Stepped (Buttons)
Best for: Permission requests, sequential setup
VStack {
OnboardingPageView(page: pages[currentPage])
.transition(.asymmetric(
insertion: .move(edge: .trailing),
removal: .move(edge: .leading)
))
HStack {
if currentPage > 0 {
Button("Back") {
withAnimation { currentPage -= 1 }
}
}
Spacer()
Button(currentPage == pages.count - 1 ? "Get Started" : "Next") {
withAnimation {
if currentPage == pages.count - 1 {
completeOnboarding()
} else {
currentPage += 1
}
}
}
.buttonStyle(.borderedProminent)
}
.padding()
}Pros:
- Clear calls to action
- Better for requesting permissions at specific steps
- Users must acknowledge each screen
Cons:
- More taps required
- Can feel slower
Persistence Patterns
@AppStorage (Simple)
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = falseWhen to use:
- Simple boolean "has seen onboarding"
- No need to track individual pages
- OK if reset on app reinstall
UserDefaults with Version
struct OnboardingStorage {
private static let key = "onboardingCompletedVersion"
private static let currentVersion = 2 // Increment to show again
static var hasCompletedOnboarding: Bool {
get { UserDefaults.standard.integer(forKey: key) >= currentVersion }
set { UserDefaults.standard.set(newValue ? currentVersion : 0, forKey: key) }
}
}When to use:
- Want to show onboarding again after major updates
- Need to track which version user saw
Keychain (Persists Reinstall)
// Use KeychainAccess or similar library
let keychain = Keychain(service: "com.yourapp.onboarding")
let hasCompleted = keychain["completed"] != nilWhen to use:
- Must persist across reinstalls
- Subscription apps where reinstall shouldn't reset
Presentation Patterns
Full Screen Cover
.fullScreenCover(isPresented: $showOnboarding) {
OnboardingView(onComplete: { showOnboarding = false })
}Best for:
- Immersive onboarding
- When main UI shouldn't be visible
Sheet
.sheet(isPresented: $showOnboarding) {
OnboardingView()
.interactiveDismissDisabled() // Prevent swipe to dismiss
}Best for:
- Less intrusive feel
- macOS apps (sheets are more common)
Inline
if !hasCompletedOnboarding {
OnboardingView(onComplete: { hasCompletedOnboarding = true })
} else {
MainContentView()
}Best for:
- Simple apps
- When you want immediate transition
Animation Patterns
Page Transitions
OnboardingPageView(page: pages[currentPage])
.id(currentPage) // Force view recreation
.transition(.asymmetric(
insertion: .move(edge: .trailing).combined(with: .opacity),
removal: .move(edge: .leading).combined(with: .opacity)
))
.animation(.easeInOut(duration: 0.3), value: currentPage)Image Animations
Image(systemName: page.imageName)
.font(.system(size: 100))
.symbolEffect(.bounce, value: isAnimating)
.onAppear { isAnimating = true }Progress Indicator
// Custom progress bar
GeometryReader { geometry in
Rectangle()
.fill(Color.accentColor)
.frame(width: geometry.size.width * CGFloat(currentPage + 1) / CGFloat(pages.count))
.animation(.easeInOut, value: currentPage)
}
.frame(height: 4)Accessibility Patterns
VoiceOver Support
OnboardingPageView(page: page)
.accessibilityElement(children: .combine)
.accessibilityLabel("\(page.title). \(page.description)")
.accessibilityHint("Page \(index + 1) of \(pages.count)")Dynamic Type
Text(page.title)
.font(.title) // Use semantic fonts
.minimumScaleFactor(0.7) // Allow shrinking if needed
.lineLimit(2)
Text(page.description)
.font(.body)Reduced Motion
@Environment(\.accessibilityReduceMotion) var reduceMotion
.animation(reduceMotion ? nil : .easeInOut, value: currentPage)Permission Request Integration
Request permissions at relevant onboarding steps:
struct OnboardingPage: Identifiable {
let id = UUID()
let title: String
let description: String
let imageName: String
let permissionRequest: PermissionType?
enum PermissionType {
case notifications
case location
case camera
case photos
}
}
// In OnboardingPageView
if let permission = page.permissionRequest {
Button("Enable") {
requestPermission(permission)
}
.buttonStyle(.borderedProminent)
}Content Guidelines
Screen 1: Welcome
- App name/logo
- One-line value proposition
- Warm, inviting imagery
Screen 2-3: Key Features
- One feature per screen
- Benefit-focused copy
- Relevant illustration/screenshot
Screen 4: Get Started
- Clear call to action
- Optional: Sign up / Sign in
- Optional: Permission request
Anti-Patterns to Avoid
Don't
- Show more than 5 screens
- Use walls of text
- Require all permissions upfront
- Make it unskippable with no good reason
- Show onboarding on every app update
Do
- Keep text under 2 sentences per screen
- Use illustrations/icons
- Request permissions contextually
- Provide skip option
- Use versioned persistence for updates
import SwiftUI
/// View modifier for easy onboarding integration.
///
/// Usage:
/// ```swift
/// @main
/// struct MyApp: App {
/// var body: some Scene {
/// WindowGroup {
/// ContentView()
/// .onboarding()
/// }
/// }
/// }
/// ```
///
/// The onboarding will automatically show on first launch
/// and won't appear again after completion.
struct OnboardingModifier: ViewModifier {
@State private var showOnboarding = false
func body(content: Content) -> some View {
content
.onAppear {
// Check on appear to handle app state restoration
if !OnboardingStorage.hasCompletedOnboarding {
showOnboarding = true
}
}
.fullScreenCover(isPresented: $showOnboarding) {
OnboardingView()
}
}
}
// MARK: - View Extension
extension View {
/// Shows onboarding on first app launch.
///
/// The onboarding is presented as a full screen cover and
/// won't appear again after completion.
///
/// Usage:
/// ```swift
/// ContentView()
/// .onboarding()
/// ```
func onboarding() -> some View {
modifier(OnboardingModifier())
}
}
// MARK: - Preview
#Preview("With Onboarding") {
// Reset for preview
let _ = OnboardingStorage.reset()
return Text("Main Content")
.onboarding()
}
import SwiftUI
/// Data model for an onboarding page.
///
/// Customize the pages array in OnboardingView or OnboardingStorage
/// with your app's content.
struct OnboardingPage: Identifiable, Equatable {
let id = UUID()
let title: String
let description: String
let imageName: String // SF Symbol name or asset name
let accentColor: Color
init(
title: String,
description: String,
imageName: String,
accentColor: Color = .accentColor
) {
self.title = title
self.description = description
self.imageName = imageName
self.accentColor = accentColor
}
}
// MARK: - Sample Pages
extension OnboardingPage {
/// Sample pages for preview and testing.
/// Replace with your actual onboarding content.
static let samplePages: [OnboardingPage] = [
OnboardingPage(
title: "Welcome",
description: "Thank you for downloading our app. Let's get you started with a quick tour.",
imageName: "hand.wave.fill",
accentColor: .blue
),
OnboardingPage(
title: "Stay Organized",
description: "Keep track of everything that matters to you in one place.",
imageName: "checklist",
accentColor: .green
),
OnboardingPage(
title: "Sync Everywhere",
description: "Your data syncs seamlessly across all your Apple devices.",
imageName: "icloud.fill",
accentColor: .purple
),
OnboardingPage(
title: "Get Started",
description: "You're all set! Tap the button below to start using the app.",
imageName: "arrow.right.circle.fill",
accentColor: .orange
)
]
}
import SwiftUI
/// Individual page view for onboarding.
///
/// Displays the content for a single onboarding step with
/// icon, title, and description.
struct OnboardingPageView: View {
let page: OnboardingPage
@Environment(\.colorScheme) private var colorScheme
var body: some View {
VStack(spacing: 32) {
Spacer()
// Icon
Image(systemName: page.imageName)
.font(.system(size: 80))
.foregroundStyle(page.accentColor)
.symbolRenderingMode(.hierarchical)
.accessibilityHidden(true)
// Text Content
VStack(spacing: 16) {
Text(page.title)
.font(.largeTitle)
.fontWeight(.bold)
.multilineTextAlignment(.center)
Text(page.description)
.font(.body)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.horizontal, 32)
Spacer()
Spacer()
}
.accessibilityElement(children: .combine)
.accessibilityLabel("\(page.title). \(page.description)")
}
}
// MARK: - Preview
#Preview {
OnboardingPageView(page: OnboardingPage.samplePages[0])
}
#Preview("Dark Mode") {
OnboardingPageView(page: OnboardingPage.samplePages[1])
.preferredColorScheme(.dark)
}
import Foundation
/// Onboarding persistence and configuration.
///
/// Use `hasCompletedOnboarding` to check/set onboarding status.
/// Increment `currentOnboardingVersion` to show onboarding again
/// after a major app update.
enum OnboardingStorage {
// MARK: - Configuration
/// The current onboarding version.
/// Increment this to show onboarding again after major updates.
private static let currentOnboardingVersion = 1
/// The UserDefaults key for storing completion status.
private static let completedVersionKey = "onboardingCompletedVersion"
// MARK: - Public API
/// Whether the user has completed onboarding for the current version.
///
/// Set to `true` when onboarding is completed.
/// Returns `false` if the user hasn't completed onboarding
/// or if a new onboarding version is available.
static var hasCompletedOnboarding: Bool {
get {
let completedVersion = UserDefaults.standard.integer(forKey: completedVersionKey)
return completedVersion >= currentOnboardingVersion
}
set {
let version = newValue ? currentOnboardingVersion : 0
UserDefaults.standard.set(version, forKey: completedVersionKey)
}
}
/// Reset onboarding status (for testing/debugging).
static func reset() {
UserDefaults.standard.removeObject(forKey: completedVersionKey)
}
}
// MARK: - Usage Example
/*
// Check if should show onboarding:
if !OnboardingStorage.hasCompletedOnboarding {
showOnboarding = true
}
// Mark as completed:
OnboardingStorage.hasCompletedOnboarding = true
// Reset for testing:
OnboardingStorage.reset()
// To show onboarding again after app update:
// 1. Increment `currentOnboardingVersion` above
// 2. Users will see onboarding again on next launch
*/
import SwiftUI
/// Main onboarding view with paged navigation.
///
/// Usage:
/// ```swift
/// // Option 1: Full screen cover
/// .fullScreenCover(isPresented: $showOnboarding) {
/// OnboardingView()
/// }
///
/// // Option 2: View modifier (recommended)
/// ContentView()
/// .onboarding()
/// ```
struct OnboardingView: View {
// MARK: - Configuration
/// The onboarding pages to display.
/// Customize this with your app's content.
private let pages = OnboardingPage.samplePages
/// Whether to show a skip button.
private let showSkipButton = true
// MARK: - State
@State private var currentPage = 0
@Environment(\.dismiss) private var dismiss
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = false
// MARK: - Body
var body: some View {
VStack(spacing: 0) {
// Skip button
if showSkipButton {
HStack {
Spacer()
Button("Skip") {
completeOnboarding()
}
.foregroundStyle(.secondary)
.padding()
}
}
// Pages
TabView(selection: $currentPage) {
ForEach(Array(pages.enumerated()), id: \.element.id) { index, page in
OnboardingPageView(page: page)
.tag(index)
}
}
.tabViewStyle(.page(indexDisplayMode: .always))
.indexViewStyle(.page(backgroundDisplayMode: .always))
// Get Started button (shown on last page)
VStack {
if currentPage == pages.count - 1 {
Button {
completeOnboarding()
} label: {
Text("Get Started")
.font(.headline)
.frame(maxWidth: .infinity)
.padding()
}
.buttonStyle(.borderedProminent)
.padding(.horizontal, 32)
.transition(.opacity.combined(with: .move(edge: .bottom)))
}
}
.frame(height: 80)
.animation(.easeInOut, value: currentPage)
// Safe area padding
Spacer()
.frame(height: 20)
}
.background(Color(uiColor: .systemBackground))
.interactiveDismissDisabled() // Prevent accidental dismiss
}
// MARK: - Actions
private func completeOnboarding() {
hasCompletedOnboarding = true
dismiss()
}
}
// MARK: - Preview
#Preview {
OnboardingView()
}
#Preview("Dark Mode") {
OnboardingView()
.preferredColorScheme(.dark)
}
// Note: For macOS, replace `Color(uiColor:)` with `Color(nsColor:)`
// and consider adjusting the layout for larger screens.