SwiftUI Onboarding in a UIKit App
I recently refactored the welcome screen that appears when you first open Squirrel, my hobby iOS app for keeping track of completing tasks. The initial version needed improvement, and I wanted to reuse the view for a new features screen that would appear after an update. I’m sharing my process for how this came together since shipping the changes.
In this post, I’ll describe how I created this new onboarding screen using SwiftUI in a UIKit-based app.
A note to the wise: WWDC 2021 begins tomorrow at the time of writing, and these directions could be out of date when you’re reading this! That said, this works for iOS 13 and 14, and presumably any updates to SwiftUI we see this week will only work on the latest versions of Apple’s operating systems.
To start, I designed my SwiftUI view and view model for the onboarding screen. I wanted to style it similar to some of the welcome screens that Apple uses in their applications, and as such, it needed a title, list of features, optional text, and the title for a button.
Rather than hardcoding the strings and items in the view (as I did in my first pass), I created a simple model to provide to our onboarding view. The view model contains a custom initializer that accepts what type of screen to display, allowing the data to be built into our model.
enum OnboardingType {
case start, features
}
struct OnboardingViewModel {
let title: String
let items: [OnboardingItem]
let extraText: String?
let buttonLabel: String
let navigationController: UINavigationController?
init(type: OnboardingType, navigationController: UINavigationController?=nil) {
switch type {
case .start:
// Assign values here
case .features:
// Same
}
}
}
struct OnboardingItem {
let id = UUID()
let image: String
let text: String
let accentColor: Color
}
Since we’ll be presenting our onboarding screen modally from UIKit, we’ll need to embed our SwiftUI view in a UINavigationController
. To ensure the modal can be dismissed from SwiftUI, I included a place for us to specify our navigation controller in the model.
For the view itself, I created three distinct sections: a title section, a ScrollView
for our list of features, and a section for the button that will dismiss the navigation controller. Designing the screen in this manner ensures the title and the button are always on screen and allows for the onboarding features list to be scrollable if it doesn’t fit on the device’s screen. All of these sections will be placed in a VStack
.
Each item in the list of features contains an Image
and Text
view (in iOS 14 and other OS versions released last year, you may be able to use Label
to achieve this). The Image
is hidden from accessibility features like VoiceOver since the symbol is supplementary to the text.
struct OnboardingListView: View {
let items: [OnboardingItem]
var body: some View {
VStack(alignment: .leading) {
ForEach(items, id: \.id) { item in
HStack(alignment: .center) {
Image(systemName: item.image)
.foregroundColor(item.accentColor)
.padding()
.font(.title)
.accessibility(hidden: true)
Text(item.text)
.font(.headline)
.fixedSize(horizontal: false, vertical: true)
}
}.padding([.leading, .trailing, .bottom])
}
}
}
With the list view finished, we can place it in the ScrollView
belonging to our onboarding screen. In the code below, you’ll see our (mostly) completed OnboardingView, with each of its sections built out. The button appears at the end of the VStack
, with its action accessing the UINavigationController
we specified in our model and dismissing it.
struct OnboardingView: View {
let model: OnboardingViewModel
var body: some View {
ZStack {
Color(.systemGroupedBackground)
.edgesIgnoringSafeArea(.all)
VStack {
Text(model.title)
.font(.largeTitle)
.bold()
.padding(.top, 20)
ScrollView {
OnboardingListView(items: model.items)
if let extraText = model.extraText {
Text(extraText)
.font(.headline)
.padding([.leading, .trailing, .bottom])
.multilineTextAlignment(.center)
}
}
Button(action: {
model.navigationController?.dismiss(animated: true)
}, label: {
Text(model.buttonLabel).padding()
})
.background(Color(.systemBlue))
.cornerRadius(15)
.accentColor(.white)
.padding([.bottom, .leading, .trailing])
}
}
}
}
The final touch to the OnboardingView will be to have our list of items centered vertically on the screen. Adding a GeometryReader
and embedding our items and extra text in a VStack
will ensure these views take all of the space in the ScrollView
. In other words, the list will be centered on the screen.
GeometryReader { geo in
ScrollView {
VStack {
OnboardingListView(items: model.items)
if let extraText = model.extraText {
Text(extraText)
.font(.headline)
.padding([.leading, .trailing, .bottom])
.multilineTextAlignment(.center)
}
}
.frame(minHeight: geo.size.height)
.frame(width: geo.size.width)
}
}
Once we’re done writing our SwiftUI code, we can turn to presenting the screen. Once you’ve confirmed you want to show one of these screens (perhaps after checking a UserDefaults value), we can embed our new SwiftUI view into a UIHostingController
. Because we’ve included the title in our view, we’ll also hide the navigation controller’s navigation bar, although you can customize this to however you see fit.
// We've already checked that we want to show the user the onboarding screen.
let navigation = UINavigationController()
let model = OnboardingViewModel(type: .start, navigationController: navigation)
let viewController = UIHostingController(rootView: OnboardingView(model: model))
navigation.viewControllers = [viewController]
viewController.navigationController?.navigationBar.isHidden = true
present(navigation, animated: true)
// After presenting the onboarding screen, ensure we won't show it again.
After presenting the view, make sure to record that you’ve shown the user the screen (again, UserDefaults is a good starting point) so the user doesn’t see the screen more than once.
This is just one example of how you can go about creating an onboarding screen in SwiftUI and presenting it from a UIKit-based app. If you’ve found this article useful, please let me know or share it with others!