How to create an onboarding screen in SwiftUI #2 – Implementing the Page Control

Share on facebook
Share on twitter
Share on pinterest

Hello and welcome to this tutorial! In this post, we will learn how to create an onboarding screen in SwiftUI. By doing this we learn a lot about interfacing with UIKit in SwiftUI. In the last part, we started creating our onboarding screen by implementing an UIViewController with feeding it with SwiftUI views. In this part, we will create the rest of our UI, including the “Next” button, the texts and the dotted page indicator. By doing this we will learn how to create a Binding between a UIKit object and a SwiftUI view. We also learn how to insert UIViews into SwiftUI.

Here’s what we are going to achieve:

Where we are 🏛

As a little reminder, here’s the architecture we use for creating our onboarding screen.

The overall OnboardingView contains a UIViewController, better said a UIPageViewController, which itself holds several subviews

The overall OnboardingView contains a UIViewController, better said a UIPageViewController, which itself holds several subviews

We already implemented the UIPageViewController including the SwiftUI subviews and enabled the swipe functionality.

What’s left is to create a “Next” button for manually going to the next subview of the PageViewController. In addition to that, we want to create two text sections for displaying information depending on the currently shown subview. Eventually, we want to include a Page Control for indicating the user, which page is currently being shown.

Keeping track of the current page 👁

To update the PageViewController when we click on the “Next” button (which we will implement in a moment), we need to keep track of which page of the PageViewController should be currently displayed.

To do this, let’s create a State property which serves as the “source of truth”, meaning that the PageViewController should get updated according to the value of this State.

So let’s insert a currentPageIndex State inside the OnboardingView. Since when the app launches the first page should be presented, we set the initial value to 0:

struct OnboardingView: View {
   //...
    
   @State var currentPageIndex = 0
   var body: some View {
      //...
    }
}

Depending on this State, the Text objects and the PageViewController should get updated. We’ll see how to accomplish this in a moment.

But first, let’s implement a Button for changing the State’s value when the user taps on it!

Implementing the Button and the Text objects 👷‍♂️

Wrap the PageViewController instance inside the OnboardingView into a VStack and insert a Button object.

var body: some View {
        VStack {
PageViewController(viewControllers: subviews)
                .frame(height: 600)
Button() {
            }
        }
    }

The button should contain a white arrow with a yellow, round background:

Button() {
    Image(systemName: "arrow.right")
                    .resizable()
                    .foregroundColor(.white)
                    .frame(width: 30, height: 30)
                    .padding()
                    .background(Color.orange)
                    .cornerRadius(30)
            }

If you want, you can outsource the button’s content as an external view like this:

struct OnboardingView: View {
//...
    
var body: some View {
        VStack {
PageViewController(viewControllers: subviews)
                .frame(height: 600)
Button() {
ButtonContent()
            }
        }
    }
}
struct ButtonContent: View {
var body: some View {
Image(systemName: "arrow.right")
        .resizable()
        .foregroundColor(.white)
        .frame(width: 30, height: 30)
        .padding()
        .background(Color.orange)
        .cornerRadius(30)
    }
}

As said, the currentPageIndex State needs to be updated when the user taps the button. Therefore, let’s write an action that adds one to the State’s value. But be aware that we want to display the first page again when the user reached the last page and taps the button. We can take this into account by writing a suitable if-else statement:

Button(action: {
if self.currentPageIndex+1 == self.subviews.count {
self.currentPageIndex = 0
                } else {
self.currentPageIndex += 1
                }
            }) {
ButtonContent()
            }

To see if this works properly, let’s also insert a (temporary) Text object for showing the current value of the currentPageIndex.

VStack {
PageViewController(viewControllers: subviews)
                .frame(height: 600)
Button(action: {
if self.currentPageIndex+1 == self.subviews.count {
self.currentPageIndex = 0
                } else {
self.currentPageIndex += 1
                }
            }) {
ButtonContent()
            }
Text("Currently shown page: \(currentPageIndex)")
        }

We can now try out this functionality by running a live preview and tapping on the button. You see, that every time we tap on it, the currentPageIndex State gets updated which triggers the whole view to rerender with updating our Text.

But instead of this temporary Text, we want to display two Text blocks for displaying information depending on the currently shown subview.

So let’s insert two arrays, one for the titles and one for the captions.


struct OnboardingView: View {
//...
    
var titles = ["Take some time out", "Conquer personal hindrances", "Create a peaceful mind"]
var captions =  ["Take your time out and bring awareness into your everyday life", "Meditating helps you dealing with anxiety and other psychic problems", "Regular medidation sessions creates a peaceful inner mind"]
@State var currentPageIndex = 0
var body: some View {
//...
    }
}

Next, insert two Text objects into your OnboardingView, each of it displaying a String of the arrays subscript by the currentPageIndex value. At this point, you can delete the temporary “Current Page” Text object.

VStack {
PageViewController(viewControllers: subviews)
                .frame(height: 600)
Text(titles[currentPageIndex])
                .font(.title)
Text(captions[currentPageIndex])
                .font(.subheadline)
                .foregroundColor(.gray)
                .frame(width: 300, height: 50, alignment: .leading)
                .lineLimit(nil)
//...
        }

Your preview should now look as follows:

If you want, you can run the app again and see if the Text’s get updated properly when you tap the button. As you probably already noticed, the PageViewController isn’t updating it’s displayed subview yet. Let’s change this by binding it to our currentPageIndex State!

Bind the PageViewController to the currentPageIndex State

Binding the PageViewController to the currentPageIndex State means that when the State gets updated the PageViewController gets updated too and that when the Binding gets updated the State gets updated. Doing this is pretty straight forward.

Just insert a @Binding property named currentPageIndex into the PageViewController.

struct PageViewController: UIViewControllerRepresentable {
    
    @Binding var currentPageIndex: Int
    
    //...
    
}

Since we can now refer to the currentPageIndex, we can update the updateUIViewController function for showing the subview depending on the currentPageIndex State’s value, which we’ve set to an initial value of 0.

func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
        pageViewController.setViewControllers(
            [viewControllers[currentPageIndex]], direction: .forward, animated: true)
    }

Now we have to update the initialisation of the PageViewController inside the OnboardingView with binding it to the currentPageIndex State of the OnboardingView:

var body: some View {
        VStack {
            PageViewController(currentPageIndex: $currentPageIndex, viewControllers: subviews)
                .frame(height: 600)
            //...
        }
    }

Let’s run our app again!

Now every time, we tap the “Next” button, the State gets updated which also updates the currentPageIndex Binding. What then happens is that the PageViewController gets updated which triggers the updateUIViewController function and eventually shows the next subview!

But now try to swipe..you notice that the Text’s don’t get updated! That’s because we’ve not yet told the PageViewController to update the currentPageIndex when the user swipes. Let’s change this!

Update the State when the user swipes 🆕

Updating the currentPageIndex can be done with a delegate pattern. The appropriate method to use is the didFinishAnimating function. Since, as you learned in the last part, the right place to insert delegate methods is the Coordinator subclass, let us conform this class to the UIPageViewControllerDelegate protocol and insert the didFinishAnimating method there:

class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
        
        //...
        
        func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
            if completed,
                let visibleViewController = pageViewController.viewControllers?.first,
                let index = parent.viewControllers.firstIndex(of: visibleViewController)
            {
                parent.currentPageIndex = index
            }
        }
        
    }

The method gets called when the swiping of the user is finished. When that’s the case, we then grab the shown subview and locate its position inside the viewControllers array. Depending on that value, we can then update the currentPageIndex binding which then also updates the currentPageIndex State of the OnboardingView.

Similar as we did with the data source pattern in the last tutorial, let’s assign the Coordinator as the PageViewController’s delegate inside the makeUIViewController function.

func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(
            transitionStyle: .scroll,
            navigationOrientation: .horizontal)
        
        pageViewController.dataSource = context.coordinator
        pageViewController.delegate = context.coordinator
        
        return pageViewController
    }

Okay, let’s run our app again and see if that works. Great, every time we swipe, the currentPageIndex State of the OnboardingView gets updated trough the updated Binding property of the PageViewController which eventually updates the Text objects of our OnboardingView accordingly!

Inserting an UIPageControl and updating the view’s composition 🖌

Last but not least, we want to have a dotted indicator for showing the user how many pages the PageViewController has and which one is currently being displayed.

Such an indicator is part of the UIKit framework and called UIPageControl, which is an UIView. In the last tutorial, we saw how to insert an UIViewController. Inserting UIViews into SwiftUI views is very similar to do this.

Create a new Swift file called PageControl.swift and make sure the UIKit and SwiftUI frameworks are imported. Then create a struct called PageControl and conform it to the UIViewRepresentable protocol:

import Foundation
import UIKit
import SwiftUI

struct PageControl: UIViewRepresentable {
    
}

Our page control needs to know how many pages the PageViewController has. It also needs to know which page is currently being displayed. So let’s create a property numberOfPages and create a Binding to the currentPageIndex State of the OnboardingView.

struct PageControl: UIViewRepresentable {
    
    var numberOfPages: Int
    
    @Binding var currentPageIndex: Int
    
}

Similar to the UIViewControllerRepresentable protocol, the UIViewRepresentable protocol has two mandatory functions which need to be implemented. The makeUIView and the updateUIView function. The makeUIView function is used for initialising the UIView for the first time. The updateUIView function gets called every time the UIView gets updated.

Let’s start with creating the makeUIView function (make sure you indicate to return an UIPageControl object after the arrow):

 func makeUIView(context: Context) -> UIPageControl {
        
    }

Inside this function, we create an UIPageControl. We set the number of dots by using the built-in numberOfPages property and assigning it to the numberOfPages property we declared above. We then modify the color of the PageControl and return it.

 func makeUIView(context: Context) -> UIPageControl {
        let control = UIPageControl()
        control.numberOfPages = numberOfPages
        control.currentPageIndicatorTintColor = UIColor.orange
        control.pageIndicatorTintColor = UIColor.gray

        return control
    }

We then implement the updateUIView function. Every time the currentPageIndex State of the OnboardingView gets updated the currentPageIndex Binding of the PageControl gets too. This then triggers the updateUIView function. So let’s use this function to tell the PageControl which dot of the PageControl should be highlighted when the user goes to another page.

struct PageControl: UIViewRepresentable {
    
    //...
    
    func updateUIView(_ uiView: UIPageControl, context: Context) {
        uiView.currentPage = currentPageIndex
    }
    
}

Now we just need to initialise a PageControl instance inside our OnboardingView with passing the number of pages and binding it to the currentPageIndex State:

var body: some View {
        VStack {
            //...
            PageControl(numberOfPages: subviews.count, currentPageIndex: $currentPageIndex)
        }
    }

This is a great opportunity for reordering the elements of our UIView and adding some paddings to make it look more nicely. If you want, you can just copy and paste the code below.


var body: some View {
        VStack(alignment: .leading) {
            PageViewController(currentPageIndex: $currentPageIndex, viewControllers: subviews)
                .frame(height: 600)
            Group {
                Text(titles[currentPageIndex])
                    .font(.title)
                Text(captions[currentPageIndex])
                .font(.subheadline)
                .foregroundColor(.gray)
                .frame(width: 300, height: 50, alignment: .leading)
                .lineLimit(nil)
            }
                .padding()
            HStack {
                PageControl(numberOfPages: subviews.count, currentPageIndex: $currentPageIndex)
                Spacer()
                Button(action: {
                    if self.currentPageIndex+1 == self.subviews.count {
                        self.currentPageIndex = 0
                    } else {
                        self.currentPageIndex += 1
                    }
                }) {
                    ButtonContent()
                }
            }
                .padding()
        }
    }

Awesome! Let’s run our app and try everything out!


You can download the whole source code from GitHub.

Conclusion 🎊

That’s it! We’ve successfully created our own onboarding screen in SwiftUI. We learned a lot about interfacing with UIKit and experienced the great power of this synergy. We learned how to implement UIViewControllers and UIViews into SwiftUI views and saw how to embed SwiftUI views into UIKit objects!

If you want to see more, make sure you follow us on Instagram and subscribe to our newsletter to not miss any updates, tutorials and tips about SwiftUI and more!

Have any questions about this article? Write it in the comments below 👇

4 replies on “How to create an onboarding screen in SwiftUI #2 – Implementing the Page Control”

Hey, I was wondering how to manipulate the SceneDelegate in order to load this Onboarding Screen just once. I am currently planning to store the state (showOnboarding) in a UserDefault. I’d then load the Setting in the SceneDelegate and decide if the Onboarding screen or the HomeScreen should be loaded

Was a good post but how can you make the last page of the onboard screen navigates to the HomeView after completion? The current code will display the onboard screen once but you need to run the app again to check if onboardview only runs once

Leave a Reply

Your email address will not be published. Required fields are marked *

small_c_popup.png

Covid-19 Forces you into quarantine?

Start Mastering swiftUI Today save your 33% discount

small_c_popup.png

Are you ready for a new era of iOS development?

Start learning swiftUI today - download our free e-book