🚀 Learn how to build and use this package: https://www.swiftful-thinking.com/offers/REyNLwwH
- ✅ 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
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
)
]
)
)
}
}- iOS 15.0+
- Swift 6.0+
- Xcode 16.0+
dependencies: [
.package(url: "https://github.com/SwiftfulThinking/SwiftfulOnboarding.git", branch: "main")
]Details (Click to expand)
Import the package:
import SwiftfulOnboardingCreate 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)Details (Click to expand)
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
)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 limitmax: nil(default) - Unlimited selectionsmax: 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
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)
)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
Collect user text input:
.textInput(
id: "name",
title: "What's your name?",
subtitle: "We'd love to get to know you",
textFieldKeyboardType: .default
)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"
)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()
)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"]
)Details (Click to expand)
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
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
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
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
}
)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()
}
}
}
)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)")
}
)Details (Click to expand)
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
)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
)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)
)Customize button appearance:
OnbButtonFormatData(
pressStyle: .press, // .press, .opacity, or .tap
font: .headline,
height: .verticalPadding(16), // or .fixed(50)
cornerRadius: 12
)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")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
)Animate between slides:
.slide // Horizontal slide (default)
.opacity // Fade in/out
.fade // Fade with subtle offset
.none // No animationDetails (Click to expand)
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"
)
]
)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")
}
)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"
)
]
)Details (Click to expand)
Track user progress and collect data using callback functions:
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
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
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
}
}
}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)
}
}
}Community contributions are encouraged! Please ensure that your code adheres to the project's existing coding style and structure.
- Open an issue for issues with the existing codebase.
- Open a discussion for new feature requests.
- Submit a pull request when the feature is ready.
MIT License. See LICENSE file for details.