App Onboarding Screen in SwiftUI

DevTechie Inc
Jun 11

Photo by Duncan Meyer on Unsplash

App onboarding is one of the most critical aspect in an app. App onboarding is mainly used as the first point of contact and therefore is essential for making a great first impression. As a result, it is important to make this process as simple and seamless as possible.

SwiftUI provides all tools needed to build simple yet effective app onboarding experience. In this article, we will build something like this:

We will use TabView with tabViewStyle modifier to build out our onboarding experience.

Let’s start with a model. I will use DevTechie courses model to display courses in a TabView. Model and sample data will look something like this:

struct DTCourse: Identifiable {
    var id = UUID()
    var title: String
    var subtitle: String
    var desc: String
}extension DTCourse {
    static var sample: [DTCourse] {
        [
            DTCourse(title: "Masting SwiftUI 3", subtitle: "Learn SwiftUI 3 by Example", desc: "In this course we will learn iOS app development in SwiftUI 3 from scratch."),
            DTCourse(title: "TodoList with Combine", subtitle: "Learn about Combine and SwiftUI", desc: "This course is all about using SwiftUI and Combine together."),
            DTCourse(title: "DisneyPlus Clone in SwiftUI", subtitle: "Build DisneyPlus Clone", desc: "In this course we will build fully functional DisneyPlus clone using SwiftUI."),
            DTCourse(title: "Masting SwiftUI 3", subtitle: "Learn SwiftUI 3 by Example", desc: "In this course we will learn iOS app development in SwiftUI 3 from scratch."),
            DTCourse(title: "TodoList with Combine", subtitle: "Learn about Combine and SwiftUI", desc: "This course is all about using SwiftUI and Combine together."),
            DTCourse(title: "DisneyPlus Clone in SwiftUI", subtitle: "Build DisneyPlus Clone", desc: "In this course we will build fully functional DisneyPlus clone using SwiftUI.")
        ]
    }
}
Next, we will build our UI.

struct OnboardingExample: View {
    var courses = DTCourse.sample // 1 get course data    @State private var selection = 0 // 2 state variable to store selection, this will change when user will move page by page and will also help us move pages programmatically     var body: some View {
        TabView(selection: $selection) { // 3 observer for selection
            ForEach(courses.indices) { idx in  
                VStack(spacing: 0) { 
                    Text(courses[idx].title)
                        .font(.largeTitle)
                    Rectangle()
                        .fill(Color.secondary)
                        .frame(height: 1)
                    
                    VStack(alignment: .leading) {
                        Text(courses[idx].subtitle)
                        Text(courses[idx].desc)
                            .foregroundColor(.secondary)
                        
                    }.padding()
                }
                .tag(idx)
            }
        }
        .tabViewStyle(.page)
    }
}
Code above will create pager experience for our users. We can hide bottom dots (called indexView) and place a button to move to next page. We will also move our TabView inside a VStack so button can be added at the bottom.

struct OnboardingExample: View {
    var courses = DTCourse.sample    @State private var selection = 0var body: some View {
        VStack {
            TabView(selection: $selection) {
                ForEach(courses.indices) { idx in 
                    VStack(spacing: 0) {
                        Spacer()
                        Text(courses[idx].title)
                            .font(.largeTitle)
                        Rectangle()
                            .fill(Color.secondary)
                            .frame(height: 1)
                        
                        VStack(alignment: .leading) {
                            Text(courses[idx].subtitle)
                            Text(courses[idx].desc)
                                .foregroundColor(.secondary)
                            
                        }.padding()
                        Spacer()
                    }
                    .tag(idx)
                }
            }
            .tabViewStyle(.page(indexDisplayMode: .never))
            HStack {
                Spacer()
                Button {
                    // add logic to move next
                } label: {
                    // add conditional label here
                    Text("Next")
                }
            }
            .padding()
            .foregroundColor(.primary)
        }
        
    }
}
Now we are ready to add some logic to our button. Button will increase value of selection State variable until it reaches the last index. Once last index is reached we will stop incrementing value and also swap label to display checkmark instead of a chevron. We will also wrap change of selection variable inside a withAnimation block so the whole transition of page can be animated. Let’s add this code:

struct OnboardingExample: View {
    var courses = DTCourse.sample
    @State private var selection = 0
    var body: some View {
        VStack {
            TabView(selection: $selection) {
                ForEach(courses.indices) { idx in 
                    VStack(spacing: 0) {
                        Spacer()
                        Text(courses[idx].title)
                            .font(.largeTitle)
                        Rectangle()
                            .fill(Color.secondary)
                            .frame(height: 1)
                        
                        VStack(alignment: .leading) {
                            Text(courses[idx].subtitle)
                            Text(courses[idx].desc)
                                .foregroundColor(.secondary)
                            
                        }.padding()
                        
                        Spacer()
                    }
                    .tag(idx)
                }
            }
            .tabViewStyle(.page(indexDisplayMode: .never))
            HStack {
                Spacer()
                Button {
                    withAnimation {
                        if selection < courses.count - 1 {
                            selection += 1
                        }
                    }
                } label: {
                    if selection == courses.count - 1 {
                        Image(systemName: "checkmark.circle")
                            .font(.largeTitle)
                    } else {
                        Image(systemName: "chevron.right.circle")
                            .font(.largeTitle)
                    }
                }
            }
            .padding()
            .foregroundColor(.primary)
        }
        
    }
}
Now when we have our basic skeleton ready, we will style this with some images. We will update our model with some sample image names from the asset(you can pick images of your choice or try https://undraw.co)

struct DTCourse: Identifiable {
    var id = UUID()
    var title: String
    var subtitle: String
    var desc: String
    var imageName: String = ""
}
extension DTCourse {
    static var sample: [DTCourse] {
        [
            DTCourse(title: "Masting SwiftUI 3", subtitle: "Learn SwiftUI 3 by Example", desc: "In this course we will learn iOS app development in SwiftUI 3 from scratch.", imageName: "one"),
            DTCourse(title: "TodoList with Combine", subtitle: "Learn about Combine and SwiftUI", desc: "This course is all about using SwiftUI and Combine together.", imageName: "two"),
            DTCourse(title: "DisneyPlus Clone in SwiftUI", subtitle: "Build DisneyPlus Clone", desc: "In this course we will build fully functional DisneyPlus clone using SwiftUI.", imageName: "three"),
            DTCourse(title: "Masting SwiftUI 3", subtitle: "Learn SwiftUI 3 by Example", desc: "In this course we will learn iOS app development in SwiftUI 3 from scratch.", imageName: "one"),
            DTCourse(title: "TodoList with Combine", subtitle: "Learn about Combine and SwiftUI", desc: "This course is all about using SwiftUI and Combine together.", imageName: "two"),
            DTCourse(title: "DisneyPlus Clone in SwiftUI", subtitle: "Build DisneyPlus Clone", desc: "In this course we will build fully functional DisneyPlus clone using SwiftUI.", imageName: "three"),
        ]
    }
}
Updated code with image:

struct OnboardingExample: View {
    var courses = DTCourse.sample
    
    @State private var selection = 0
    
    var body: some View {
        VStack {
            TabView(selection: $selection) {
                ForEach(courses.indices) { idx in
                    VStack(spacing: 0) {
                        Spacer()
                        Text(courses[idx].title)
                            .font(.largeTitle)
                        Rectangle()
                            .fill(Color.secondary)
                            .frame(height: 1)
                        
                        VStack(alignment: .leading) {
                            
                            Image(courses[idx].imageName)
                                .resizable()
                                .scaledToFit()
                            
                            Text(courses[idx].subtitle)
                            
                            Text(courses[idx].desc)
                                .foregroundColor(.secondary)
                            
                        }.padding()
                        
                        Spacer()
                    }
                    .tag(idx)
                }
            }
            .tabViewStyle(.page(indexDisplayMode: .never))
            HStack {
                Spacer()
                Button {
                    withAnimation {
                        if selection < courses.count - 1 {
                            selection += 1
                        }
                    }
                } label: {
                    if selection == courses.count - 1 {
                        Image(systemName: "checkmark.circle")
                            .font(.largeTitle)
                    } else {
                        Image(systemName: "chevron.right.circle")
                            .font(.largeTitle)
                    }
                }
            }
            .padding()
            .foregroundColor(.primary)
        }
        
    }
}
With that we have reached the end of this article. Thank you once again for reading. Don’t forget to subscribe to our weekly newsletter at https://www.devtechie.com