Practical SwiftUI: Onboarding UI

At WWDC this year we saw the announcement for SwiftUI, a new way to build user interfaces for our iOS applications in a declarative manner. Whilst SwiftUI is still in beta and there are likely changes to occur in the syntax, it’s felt worthwhile digging into it to prepare for the future approach for building iOS interfaces. In this article I want to take a look at building a simple screen using SwiftUI so that we can get a basic grasp of how things work.

Note: We’re not going to cover how to setup your machine / project to use SwiftUI in this article. I will presume that you are on the beta release of xcode and setup to create projects using SwiftUI.


We’re going to start by building the first user facing screen of our app here, the onboarding screen. We’re going to want this to essentially welcome the user to our application, presenting them with what our app does along with an option to continue onto the authentication screen. For the end result we’re going to want to have something like this:

Even though there isn’t too much that makes up this screen, there’s enough there for us to be able to start exploring Swift UI and seeing some of the things that it can do for our applications. Let’s start by pulling about the code that makes up this screen. We’ll begin with the parent View itself:

struct OnboardingView: View {
    var body: some View {
            
    }
}

We start here by defining a struct which adheres to the View protocol – this requires us to define a body which is to contain the content of our view that we wish to display on screen. It is here that we will declare the various UI components that make up our onboarding screen – and wherever we wish to reference this onboarding screen we will do so by using the OnboardingView name given to the struct.

For our onboarding screen you may have noticed we had a purple background, along with some content placed on top. For this reason we’re going to make use of the ZStack component which allows us to stack items on-top of each other, with the z-index defined by the order in which the children are declared. Here we define a new Rectangle component, assign in a foreground color and declare that it should span the entire space of the device using edgesIgnoringSafeArea:

struct OnboardingView: View {
    var body: some View {
        ZStack {
            Rectangle().foregroundColor(Color("primary"))
                .edgesIgnoringSafeArea(.all)
        }
    }
}

At this point we have our background setup, but we’re not yet displaying any content on top of it – for this we’re going to want to add a container to hold our child views, so we’ll add a VStack component to our ZStack:

struct OnboardingView: View {
    var body: some View {
        ZStack {
            Rectangle().foregroundColor(Color("primary"))
                .edgesIgnoringSafeArea(.all)
            VStack {
   
            }
        }
    }
}

This VStack allows us to display child components, stacked in a vertical manner in the order that they are defined. To see this in action, let’s go ahead and add some title text to our screen:

VStack {
    Text("minimise")
        .font(Font.custom("Poppins-Bold", size: 36))
        .foregroundColor(Color.white)
}

We’ve added a Text component here, providing the content that we wish to display with the constructor. This Text component provides us with many operations which can be used to control the look and feel, for now I’m just going to set a custom font and assign a foreground color to be used for the content of the component.


Next on our screen we have the three headed sections which showcases the main features of our app. Because we’re going to want to show several of these view components on the screen it would be ideal if we create a reusable view component so that we don’t duplicate the code for this part of our screen. For these we’re going to define a new struct for our component that extends from View. Because this component is going to be showcasing a feature of our application, let’s go ahead and call this ApplicationFeature:

struct ApplicationFeature : View {

}

At this point we have our view, but a) it’s not displaying anything and b) it doesn’t have any data to display. Let’s add a constructor to our component so that we can provide some data that is specific to an instance of an ApplicationFeature:

struct ApplicationFeature : View {

    var featureTitle: String
    var featureDescription: String

    init(
        featureTitle: String, 
        featureDescription: String
    ) {
        self.featureTitle = featureTitle
        self.featureDescription = featureDescription
    }
}

You can see here that we define two string variables that each represent a detail for the feature of our application – a title and a description. We need to populate these when instantiating an instance of our component so here we define a constructor that takes arguments for each of these and then assign them to their respective variables.

Now that we can instantiate an instance of our view component, we need to go ahead and add a body for our view so that content is actually displayed on-screen to our user.

struct ApplicationFeature : View {
    ...

    var body: some View {
        VStack(alignment: .center) {
            Text(self.featureTitle)
                .font(Font.custom("Poppins-Medium", size: 20))
                .foregroundColor(Color.white)
                .bold()
                .padding(.bottom)
            Text(self.featureDescription)
                .font(Font.custom("Poppins-Regular", size: 16))
                .foregroundColor(Color.white)
                .multilineTextAlignment(.center)
                .padding(.bottom, 42)
        }
        .padding(.leading, 36)
        .padding(.trailing, 36)
    }
}

We can see here that we’ve made use of a couple of components to display our content in a horiztontal format, with some vertical constrained content inside of that – resulting in something that looks like the below:

Let’s break this down a bit so we can understand what is going on here. Starting at the top, we begin with the VStack container – for this we’ve added the alignment property so that the children of this view will be aligned to the start of their parent – resulting in our text being aligned in that fashion. Next, we add our two Text components by passing in each of the text variables to their constructor so that the textual content is displayed within the Text views. You may notice here that we’ve also applied the several styling properties to both of our Text components – most of these are self explanatory from their naming!

With this all in place, we now have a custom component which allows us to promote code re-use with the Swift UI approach being used for our onboarding screen.


At this point we may have the component created, but it’s not actually being used anywhere. We’ll go ahead and add several ApplicationFeature views to our layout, passing in the corresponding content for each one:

struct OnboardingView: View {
    var body: some View {
        ZStack {
            Rectangle().foregroundColor(Color("primary"))
                .edgesIgnoringSafeArea(.all)
            VStack {
                Text("minimise")
                    .font(Font.custom("Poppins-Bold", size: 36))
                    .foregroundColor(Color.white)
                Spacer()
                ApplicationFeature(featureTitle: "Track belongings", 
                    featureDescription: "Some description")
                ApplicationFeature(featureTitle: "Belonging 
                    awareness", 
                    featureDescription: "Some description")
                ApplicationFeature(featureTitle: "Know what you're 
                    using", 
                    featureDescription: "Some description")
                Spacer()  
            }
        }
    }
}

You can hopefully see here how much code duplication this has saved us from. Whilst the ApplicationFeature is something that we’ll likely only use on this onboarding screen here, there are bound to be custom view components which you’ll want to make and re-use throughout your application – this will work in exactly the same way.

You may also notice that we’ve added this Spacer component – this is used to add flexible spacing to our UI. This component will expand along the major axis of the parent component that it is contained within. We’ll place one of these both before and after our ApplicationFeature components so that the three sections of our onboarding (title, content and button) are evenly distributed on-screen.

Now that we’ve added these view components added to our layout, let’s finish off the rest of the screen and add a simple button so that the use can continue to the next screen and authenticate with our application.

struct OnboardingView: View {
    var body: some View {
        ZStack {
            Rectangle().foregroundColor(Color("primary"))
                .edgesIgnoringSafeArea(.all)
            VStack {
                Text("minimise")
                    .font(Font.custom("Poppins-Bold", size: 36))
                    .foregroundColor(Color.white)
                Spacer()
                ApplicationFeature(featureTitle: "Track belongings", featureDescription: "Log all of your belongs and track when each one is used.")
                ApplicationFeature(featureTitle: "Belonging awareness", featureDescription: "When trying to add new belongings to the app, we'll let you know if you already have something similar")
                ApplicationFeature(featureTitle: "Know what you're using", featureDescription: "We'll make you aware when you haven't used something for a while, helping to rediscover or reconsider your belongings")
                Spacer() 
                Button(action: {}) {
                    Text("Continue")
                        .foregroundColor(Color.white)
                        .font(Font.custom("Poppins-Bold", size: 18))
                }.padding(.all)
                .background(Color("primary_dark"))
                .cornerRadius(8)      
            }
        }
    }
}

Finally, we’ve defined a Button view component, for now providing an empty action property for when that button is interacted with. In this post I don’t want to handle that logic for completing that flow, but this would be the point where you will navigate to the next screen of the application. For the content of the Button itself, we’ve added a Text child component so that we can diaply some content on the button itself, along with some padding to provide a sufficient touch area for this component.


In this article we’ve taken a look at how we can build a simple onboarding screen for our application, consisting of components from SwiftUI. I hope this has been enough to introduce you to SwiftUI – in the next article we’ll be taking a look at the authentication screen, diving a bit deeper into what we can achieve with SwiftUI!

[twitter-follow screen_name=’hitherejoe’ show_count=’yes’]