Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Open more actions menu

Repository files navigation

🚀 Learn how to build and use this package: https://www.swiftful-thinking.com/offers/REyNLwwH

SwiftfulOnboarding 👋

Beautiful, customizable onboarding flows for SwiftUI applications.

  • ✅ Multiple slide types (Regular, Multiple Choice, Yes/No, Rating, Text Input, Date Picker, Picker, Primary Action)
  • ✅ Real-time feedback & response screens
  • ✅ Dynamic slide insertion based on user responses
  • ✅ Fully customizable styling and layouts
  • ✅ Built-in support for images (remote & bundle), Lottie animations, and system icons
  • ✅ Progress tracking with callbacks

Quick Start (TLDR)

Details (Click to expand)

Create a simple onboarding flow:

import SwiftfulOnboarding

struct ContentView: View {
    var body: some View {
        SwiftfulOnboardingView(
            configuration: OnbConfiguration(
                slides: [
                    .regular(
                        id: "welcome",
                        title: "Welcome!",
                        subtitle: "Get started with our amazing app",
                        media: .systemIcon(named: "star.fill")
                    ),
                    .multipleChoice(
                        id: "interests",
                        title: "What are you interested in?",
                        options: [
                            OnbChoiceOption(id: "tech", content: OnbButtonContentData(text: "Technology")),
                            OnbChoiceOption(id: "design", content: OnbButtonContentData(text: "Design")),
                            OnbChoiceOption(id: "business", content: OnbButtonContentData(text: "Business"))
                        ]
                    ),
                    .rating(
                        id: "rate",
                        title: "How excited are you?",
                        contentAlignment: .top,
                        ratingButtonOption: .number
                    )
                ]
            )
        )
    }
}

Requirements

  • iOS 15.0+
  • Swift 6.0+
  • Xcode 16.0+

Installation

Swift Package Manager

dependencies: [
    .package(url: "https://github.com/SwiftfulThinking/SwiftfulOnboarding.git", branch: "main")
]

Setup

Details (Click to expand)

Import the package:

import SwiftfulOnboarding

Create an onboarding configuration:

let config = OnbConfiguration(
    headerConfiguration: OnbHeaderConfiguration(
        headerStyle: .progressBar,
        headerAlignment: .center,
        showBackButton: .afterFirstSlide
    ),
    slides: [
        // Your slides here
    ]
)

Display the onboarding view:

SwiftfulOnboardingView(configuration: config)

Slide Types

Details (Click to expand)

Regular Slide

Simple informational slide with optional media:

.regular(
    id: "welcome",
    title: "Welcome to Our App",
    subtitle: "Discover amazing features",
    media: .systemIcon(named: "star.fill"),
    mediaPosition: .top,
    contentAlignment: .center
)

Multiple Choice Slide

Present users with multiple options:

.multipleChoice(
    id: "interests",
    title: "Select your interests",
    subtitle: "Choose all that apply",
    options: [
        OnbChoiceOption(
            id: "tech",
            content: OnbButtonContentData(
                text: "Technology",
                secondaryContent: .media(media: .systemIcon(named: "laptopcomputer", size: .small))
            )
        ),
        OnbChoiceOption(
            id: "design",
            content: OnbButtonContentData(
                text: "Design",
                secondaryContent: .media(media: .systemIcon(named: "paintbrush", size: .small))
            )
        ),
        OnbChoiceOption(
            id: "business",
            content: OnbButtonContentData(
                text: "Business",
                secondaryContent: .media(media: .systemIcon(named: "briefcase", size: .small))
            )
        )
    ],
    selectionBehavior: .multi(max: 3),
    contentAlignment: .top
)

Multiple choice slides support:

  • Single selection: .single(autoAdvance: Bool) - Select one option
  • Multi selection: .multi(max: Int?) - Select multiple options with optional maximum limit
    • max: nil (default) - Unlimited selections
    • max: 3 - Limit to maximum 3 selections
  • Grid layout: Display options in a grid
  • Custom button styles: Duolingo-style, solid, outline, solidOutline
  • Checkboxes: Circle or square checkboxes for multi-select

Yes/No Slide

Binary choice with custom labels:

.yesNo(
    id: "notifications",
    title: "Enable Notifications?",
    subtitle: "Stay updated with the latest news",
    yesOption: OnbChoiceOption(id: "yes", content: OnbButtonContentData(text: "Yes, please")),
    noOption: OnbChoiceOption(id: "no", content: OnbButtonContentData(text: "Maybe later")),
    selectionBehavior: .single(autoAdvance: true)
)

Rating Slide

Collect user ratings:

.rating(
    id: "satisfaction",
    title: "How satisfied are you?",
    subtitle: "Your feedback helps us improve",
    contentAlignment: .top,
    ratingButtonOption: .number,  // or .thumbs
    ratingLabels: RatingFooterLabels(
        left: "Poor",
        right: "Excellent"
    ),
    selectionBehavior: .single(autoAdvance: false)
)

Rating options:

  • Number rating: 1-5 scale with numbers
  • Thumbs rating: Thumbs up/down (2-point scale)
  • Custom labels: Add context to rating endpoints
  • Label placement: Top or bottom of rating buttons

Text Input Slide

Collect user text input:

.textInput(
    id: "name",
    title: "What's your name?",
    subtitle: "We'd love to get to know you",
    textFieldKeyboardType: .default
)

Primary Action Slide

Call-to-action with optional secondary button:

.primaryAction(
    id: "get-started",
    title: "You're All Set!",
    subtitle: "Ready to begin your journey?",
    media: .systemIcon(named: "checkmark.circle.fill"),
    contentAlignment: .center,
    ctaText: "Get Started",
    secondaryButtonText: "Skip for now"
)

Date Picker Slide

Collect a date or time from the user:

.datePicker(
    id: "birthday",
    title: "When is your birthday?",
    subtitle: "We'll celebrate with you!",
    datePickerPosition: .auto,       // .auto or .bottom
    datePickerStyle: .graphical,     // .graphical, .wheel, or .compact
    datePickerComponents: .date,     // .date, .dateTime, or .time
    datePickerStartDate: Date(),
    datePickerMinimumDate: nil,
    datePickerMaximumDate: Date()
)

Picker Slide

Present a list of string options in a picker:

.picker(
    id: "language",
    title: "What's your preferred language?",
    pickerPosition: .auto,     // .auto or .bottom
    pickerStyle: .wheel,       // .wheel, .menu, or .segmented
    pickerOptions: ["English", "Spanish", "French", "German", "Japanese"]
)

Advanced Features

Details (Click to expand)

Feedback Configuration

Show inline feedback based on user selections:

.multipleChoice(
    id: "quiz",
    title: "What is 2 + 2?",
    options: [
        OnbChoiceOption(
            id: "correct",
            content: OnbButtonContentData(text: "4"),
            feedbackConfiguration: OnbFeedbackConfiguration(
                backgroundColor: .green.opacity(0.2),
                cornerRadius: 4,
                title: "Correct!",
                subtitle: "Great job"
            )
        ),
        OnbChoiceOption(
            id: "incorrect",
            content: OnbButtonContentData(text: "5"),
            feedbackConfiguration: OnbFeedbackConfiguration(
                backgroundColor: .red.opacity(0.2),
                cornerRadius: 4,
                title: "Oops!",
                subtitle: "Try again"
            )
        )
    ],
    feedbackStyle: .top()  // or .bottom()
)

Feedback styles:

  • .top(transition: .none/.slide/.opacity) - Appears above content
  • .bottom(transition: .none/.slide/.opacity) - Appears above rating/options

Response Configuration

Show full-screen response screens:

.multipleChoice(
    id: "subscribe",
    title: "Choose your plan",
    options: [
        OnbChoiceOption(
            id: "premium",
            content: OnbButtonContentData(text: "Premium Plan"),
            responseConfiguration: OnbResponseConfiguration(
                style: .center(transition: .slide),
                backgroundColor: .blue,
                horizontalPadding: 24,
                title: "Welcome to Premium!",
                titleFont: .largeTitle,
                subtitle: "You've made an excellent choice",
                ctaText: "Continue",
                ctaButtonStyle: .solid(backgroundColor: .white, textColor: .blue)
            )
        )
    ],
    selectionBehavior: .single(autoAdvance: true)
)

Response styles:

  • .center(transition: .slide/.opacity/.fade/.scale) - Centered full-screen
  • .bottom(transition: .bottom) - Bottom sheet style

Dynamic Slide Insertion

Insert slides based on user responses:

.multipleChoice(
    id: "experience",
    title: "How experienced are you?",
    options: [
        OnbChoiceOption(
            id: "beginner",
            content: OnbButtonContentData(text: "Beginner"),
            insertConfiguration: [
                InsertSlideData(
                    placement: .next,
                    slide: .regular(
                        id: "beginner-tip",
                        title: "Tips for Beginners",
                        subtitle: "Here's what you need to know"
                    )
                )
            ]
        ),
        OnbChoiceOption(
            id: "expert",
            content: OnbButtonContentData(text: "Expert"),
            insertConfiguration: [
                InsertSlideData(
                    placement: .next,
                    slide: .regular(
                        id: "expert-features",
                        title: "Advanced Features",
                        subtitle: "Unlock your full potential"
                    )
                )
            ]
        )
    ]
)

Insert placements:

  • .next - Insert immediately after current slide
  • .afterSlide(id: String) - Insert after a specific slide ID
  • .afterCount(count: Int) - Insert N slides after the current slide

Dynamic Rating Configuration

The rating slide supports closures that return different configurations per rating value:

.rating(
    id: "satisfaction",
    title: "How satisfied are you?",
    ratingButtonOption: .number,
    getResponseConfiguration: { rating in
        if rating >= 4 {
            return OnbResponseConfiguration(
                backgroundColor: .green,
                title: "Glad you love it!",
                ctaText: "Continue"
            )
        }
        return nil
    },
    getFeedbackConfiguration: { rating in
        if rating <= 2 {
            return OnbFeedbackConfiguration(
                backgroundColor: .red.opacity(0.2),
                title: "We're sorry to hear that"
            )
        }
        return nil
    },
    getInsertConfiguration: { rating in
        if rating <= 2 {
            return [
                InsertSlideData(
                    placement: .next,
                    slide: .textInput(
                        id: "improvement",
                        title: "How can we improve?"
                    )
                )
            ]
        }
        return nil
    }
)

Primary Action with Async Work

Use onDidPressPrimaryButton to perform async work (e.g. requesting permissions) before the slide advances:

.primaryAction(
    id: "notifications",
    title: "Stay in the Loop",
    subtitle: "Get notified about important updates",
    media: .systemIcon(named: "bell.fill"),
    ctaText: "Enable Notifications",
    secondaryButtonText: "Not now",
    onDidPressPrimaryButton: { completion in
        // Request notification permission, then call completion to advance
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { _, _ in
            DispatchQueue.main.async {
                completion()
            }
        }
    }
)

Progress Callbacks

Track user progress through the flow:

let config = OnbConfiguration(
    slides: slides,
    onSlideComplete: { slideData in
        print("Completed slide: \(slideData.slideId)")
        print("Selections: \(slideData.selections)")
    },
    onFlowComplete: { flowData in
        print("Onboarding complete!")
        print("Total slides: \(flowData.slides.count)")
    }
)

Customization

Details (Click to expand)

Slide Defaults

Set default styling for all slides:

OnbConfiguration(
    slideDefaults: OnbSlideDefaults(
        titleFont: .system(.title, weight: .bold),
        subtitleFont: .body,
        titleAlignment: .center,
        contentAlignment: .center,
        paddingTop: 40,
        paddingBottom: 0,
        horizontalPaddingContent: 24,
        contentSpacing: 12,
        ctaButtonStyle: .solid(backgroundColor: .blue, textColor: .white),
        ctaButtonFormatData: OnbButtonFormatData(
            pressStyle: .press,
            font: .headline,
            height: .verticalPadding(16),
            cornerRadius: 12
        ),
        background: .solidColor(.clear),
        transitionStyle: .slide
    ),
    slides: slides
)

Header Configuration

Customize the header:

OnbHeaderConfiguration(
    headerStyle: .progressBar,  // .progressBar, .dots, .count, or .none
    headerAlignment: .center,   // .center or .right
    showBackButton: .afterFirstSlide,  // .always, .afterFirstSlide, or .never
    backButtonColor: .blue,
    progressBarAccentColor: .blue
)

Button Styles

Multiple button style options:

// Solid style
.solid(
    backgroundColor: Color(uiColor: .systemGray5),
    textColor: .black,
    selectedBackgroundColor: .blue,
    selectedTextColor: .white
)

// Outline style
.outline(
    textColor: .blue,
    borderColor: .blue,
    borderWidth: 2,
    selectedTextColor: .white,
    selectedBorderColor: .blue
)

// Solid with outline
.solidOutline(
    backgroundColor: .white,
    textColor: .black,
    borderColor: .gray,
    borderWidth: 1,
    selectedBackgroundColor: .blue,
    selectedTextColor: .white,
    selectedBorderColor: .blue
)

// Duolingo style (3D button with shadow)
.duolingo(
    backgroundColor: .green,
    textColor: .white,
    shadowColor: .green.opacity(0.6),
    selectedBackgroundColor: .green.opacity(0.8),
    selectedTextColor: .white,
    selectedShadowColor: .green.opacity(0.4)
)

Button Format

Customize button appearance:

OnbButtonFormatData(
    pressStyle: .press,  // .press, .opacity, or .tap
    font: .headline,
    height: .verticalPadding(16),  // or .fixed(50)
    cornerRadius: 12
)

Background Types

Set slide backgrounds:

// Solid color
.solidColor(.blue)

// Gradient
.gradient(
    Gradient(colors: [.purple, .blue]),
    startPoint: .topLeading,
    endPoint: .bottomTrailing
)

// Image
.image(urlString: "https://example.com/background.jpg")

Media Types

Support for various media types:

// System icon
.systemIcon(named: "star.fill", size: .large)

// Remote image (loaded via URL)
.image(urlString: "https://example.com/image.jpg", size: .medium)

// Bundle image (from asset catalog)
.bundleImage(named: "onboarding_hero", size: .medium)

// Video
.video(urlString: "https://example.com/video.mp4", size: .large, loop: true)

// Lottie animation
.lottie(urlString: "https://example.com/animation.json", size: .medium, loopMode: .loop)

Media sizes: .auto, .small, .medium, .large, .fixed(width:height:)

Aspect ratios (image, bundleImage, video, lottie): .auto, .square, .portrait, .landscape

Image, bundleImage, video, and lottie types also support border options:

.image(
    urlString: "https://example.com/image.jpg",
    size: .medium,
    aspectRatio: .square,
    cornerRadius: 12,
    borderColor: .gray,
    borderWidth: 2,
    selectedBorderColor: .blue,
    selectedBorderWidth: 3
)

// Bundle images support the same options
.bundleImage(
    named: "my_image",
    size: .medium,
    aspectRatio: .square,
    cornerRadius: 12,
    borderColor: .gray,
    borderWidth: 2,
    selectedBorderColor: .blue,
    selectedBorderWidth: 3
)

Transition Styles

Animate between slides:

.slide   // Horizontal slide (default)
.opacity // Fade in/out
.fade    // Fade with subtle offset
.none    // No animation

Example Flows

Details (Click to expand)

Simple Informational Flow

OnbConfiguration(
    slides: [
        .regular(
            id: "welcome",
            title: "Welcome",
            subtitle: "Get started with our app"
        ),
        .regular(
            id: "features",
            title: "Key Features",
            subtitle: "Discover what you can do"
        ),
        .regular(
            id: "ready",
            title: "Ready to Go!",
            subtitle: "Let's dive in"
        )
    ]
)

Interactive Survey Flow

OnbConfiguration(
    slides: [
        .multipleChoice(
            id: "interests",
            title: "What interests you?",
            options: [
                OnbChoiceOption(id: "tech", content: OnbButtonContentData(text: "Technology")),
                OnbChoiceOption(id: "design", content: OnbButtonContentData(text: "Design")),
                OnbChoiceOption(id: "business", content: OnbButtonContentData(text: "Business"))
            ],
            selectionBehavior: .multi(max: 3)
        ),
        .rating(
            id: "satisfaction",
            title: "How likely are you to recommend us?",
            contentAlignment: .top,
            ratingButtonOption: .number
        ),
        .textInput(
            id: "feedback",
            title: "Any additional feedback?",
            textFieldKeyboardType: .default
        )
    ],
    onFlowComplete: { flowData in
        print("Survey complete: \(flowData.slides.count) slides")
    }
)

Conditional Flow with Dynamic Slides

OnbConfiguration(
    slides: [
        .yesNo(
            id: "notifications",
            title: "Enable Notifications?",
            yesOption: OnbChoiceOption(
                id: "yes",
                content: OnbButtonContentData(text: "Yes"),
                insertConfiguration: [
                    InsertSlideData(
                        placement: .next,
                        slide: .multipleChoice(
                            id: "notification-types",
                            title: "What notifications?",
                            options: [
                                OnbChoiceOption(id: "all", content: OnbButtonContentData(text: "All")),
                                OnbChoiceOption(id: "important", content: OnbButtonContentData(text: "Important Only"))
                            ]
                        )
                    )
                ]
            ),
            noOption: OnbChoiceOption(id: "no", content: OnbButtonContentData(text: "No"))
        ),
        .primaryAction(
            id: "complete",
            title: "All Set!",
            ctaText: "Get Started"
        )
    ]
)

Tracking User Progress

Details (Click to expand)

Track user progress and collect data using callback functions:

onSlideComplete

Called each time a user completes a slide (by clicking Continue or auto-advancing):

OnbConfiguration(
    slides: slides,
    onSlideComplete: { slideData in
        print("User completed slide: \(slideData.slideId)")
        print("Slide type: \(slideData.slideType)")
        print("Selections: \(slideData.selections)")

        // Example: Save progress to UserDefaults
        UserDefaults.standard.set(slideData.slideId, forKey: "lastCompletedSlide")

        // Example: Send analytics event
        analytics.track("slide_completed", properties: slideData.eventParameters)
    }
)

Parameters:

  • slideData: OnbSlideData - Contains slide info and user selections:
    • .slideId: String - The ID of the slide that was just completed
    • .slideTitle: String? - The title of the slide
    • .slideType: String - The type of slide (e.g. "regular", "multipleChoice")
    • .selections: [OnbSelectionData] - User selections for this slide
    • .eventParameters: [String: Any] - Pre-formatted dictionary for analytics

Use cases:

  • Track user progress through the flow
  • Save partial completion state
  • Send analytics events per slide
  • Update UI outside the onboarding flow

onFlowComplete

Called when the user completes the entire onboarding flow (reaches the last slide and clicks Continue):

OnbConfiguration(
    slides: slides,
    onFlowComplete: { flowData in
        print("Onboarding complete!")
        print("Total slides: \(flowData.slides.count)")

        // Example: Process each slide's data
        for slide in flowData.slides {
            print("\(slide.slideId): \(slide.selections.map { $0.id })")
        }

        // Example: Navigate to main app
        isOnboardingComplete = true

        // Example: Send completion event
        analytics.track("onboarding_completed", properties: flowData.eventParameters)
    }
)

Parameters:

  • flowData: OnbFlowData - Contains data from the entire flow:
    • .slides: [OnbSlideData] - Array of all slide data in completion order
    • .eventParameters: [String: Any] - Pre-formatted dictionary for analytics

Use cases:

  • Save all user preferences at once
  • Navigate to the main app
  • Create user profile from collected data
  • Send completion analytics
  • Trigger welcome emails or notifications

Accessing Selection Data

Each OnbSelectionData in the selections contains:

struct OnbSelectionData {
    var id: String          // Option identifier
    var text: String?       // Option text
    var value: Any?         // Custom value you can attach
}

Example: Processing selections

onFlowComplete: { flowData in
    for slide in flowData.slides {
        switch slide.slideId {
        case "interests":
            let interestIds = slide.selections.map { $0.id }
            let interestTexts = slide.selections.compactMap { $0.text }
            print("User interests: \(interestTexts)")

        case "satisfaction":
            if let rating = slide.selections.first?.value as? Int {
                print("User rated: \(rating)/5")
            }

        case "name":
            if let name = slide.selections.first?.text {
                print("User name: \(name)")
            }

        case "notifications":
            let enabled = slide.selections.first?.id == "yes"
            print("Notifications enabled: \(enabled)")

        default:
            break
        }
    }
}

Complete Example

struct OnboardingCoordinator: View {
    @State private var showOnboarding = true
    @State private var userProfile: UserProfile?

    var body: some View {
        if showOnboarding {
            SwiftfulOnboardingView(
                configuration: OnbConfiguration(
                    slides: onboardingSlides,
                    onSlideComplete: { slideData in
                        // Track progress
                        print("Completed: \(slideData.slideId)")
                    },
                    onFlowComplete: { flowData in
                        // Process all data
                        userProfile = UserProfile(from: flowData)

                        // Dismiss onboarding
                        showOnboarding = false
                    }
                )
            )
        } else {
            MainAppView(userProfile: userProfile)
        }
    }
}

Contributing

Community contributions are encouraged! Please ensure that your code adheres to the project's existing coding style and structure.

License

MIT License. See LICENSE file for details.

About

Config-based onboarding flows for SwiftUI

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages

Morty Proxy This is a proxified and sanitized view of the page, visit original site.