How to create an onboarding screen in SwiftUI #1 – Embedding a UIPageViewController

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 this part, we will create our onboarding screen by implementing an UIViewController with feeding it with SwiftUI views. By doing this, we will learn how to integrate UIViewControllers into SwiftUI and how to transform SwiftUI Views into UIViewControllers!

Here’s what we are going to achieve:


To get started, create a new Xcode project called “Onboarding” and store it wherever you want. Make sure “Use SwiftUI” is selected. After creating the project, import the images into your Assets.xassets folder and make sure you name them correctly.

Our app’s architecture 🏛

Before diving into the coding part, let’s discuss our onboarding app’s architecture.

It simply consists of one single SwiftUI Content View, called OnboardingView. This content view contains two SwiftUI Text objects and an “arrow” Button. But it also contains a UIPageViewController (which is part of the UIKit), which itself contains three different subviews. The subviews are plain SwiftUI views, but which need to be transformed into UIViewControllers because the embedded UIPageViewControllers only accepts UIViewControllers. In addition to that, the OnboardingView contains an UIPageControl to indicate the current page that’s displayed, which is also part of the UIKit framework.

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

For creating this SwiftUI-UIKit synergy based app, it’s best practice to go from detail to general, meaning that we start with creating our subviews, which we then use to feed our UIPageViewController.

Creating the subviews 🖌

As you saw in the graphic above, the subviews are of a very simple composition. They only consist of one overall image. We could create one single SwiftUI view for every subview but in this example, it’s more efficient to compose a single one and initialise it multiple times with different images.

So let’s create a File-New-File, select SwiftUI view and click on “Next”, name it Subview and then click on “Create”.

As said, we want to dynamically display different images when initialising this view later on. So let’s declare a variable imageString for this purpose.

import SwiftUI

struct Subview: View {
    var imageString: String
    var body: some View {
        //...
    }
}

Next, insert an Image object with passing the imageString variable. To properly frame the image, also add some modifiers.

var body: some View {
        Image(imageString)
        .resizable()
        .aspectRatio(contentMode: .fill)
        .clipped()
    }

Now we need to update our previews struct for showing a sample image.

struct Subview_Previews: PreviewProvider {
    static var previews: some View {
        Subview(imageString: "meditating")
    }
}

This is how your subview should look like now:

That’s all! Theoretically, we could now use this view for creating our subviews.

But as said, we can’t directly use SwiftUI views for feeding a UIPageViewController. Instead, we first need to transform them into UIViewControllers.

Transforming SwiftUI Views into UIViewControllers 🔄

But first, we have to create our onboarding view, especially for holding our UIPageViewController, as you saw in the App’s hierarchy section. To do this, create a new SwiftUI file named OnboardingView.

As mentioned, the OnboardingView will be the place to embed our UIPageViewController in.

Because of this, it’s also the right place to initialise our subviews for passing them to the UIPageViewController (which we will create in a moment). So let’s create an array for storing our subviews:

struct OnboardingView: View {
    
    var subviews = [
        
    ]
    
    var body: some View {
        //...
    }
}

Because our UIPageViewcontroller will only accept UIViewControllers we have to transform our subviews into UIViewControllers. We can simply do that by declaring an UIHostingController for every subview respectively. UIHostingControllers are used for wrapping SwiftUI views in order to use them within the UIKit framework.

struct OnboardingView: View {
    
    var subviews = [
        UIHostingController(rootView: Subview(imageString: "meditating")),
        UIHostingController(rootView: Subview(imageString: "skydiving")),
        UIHostingController(rootView: Subview(imageString: "sitting"))
    ]
    
    var body: some View {
        //...
    }
}

At this point, you can delete the default ContentView.swift file since we won’t need it anymore. But then we have to set our OnboardingView as the root view when the app launches. We do this within the scene function inside the scenedelegate.swift file.

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: OnboardingView())
            self.window = window
            window.makeKeyAndVisible()
        }
    }

Great, we successfully prepared our subviews for feeding our UIPageViewController, which we will create now!

Creating the UIPageViewController 🚀

So we just learned how to convert SwiftUI views into UIViewControllers using an UIHostingController. But how can we embed a ViewController of the UIKit framework, like a PageViewController is one, into our SwiftUI OnboardingView?

First of all, let’s create a Swift file called PageViewController. Make sure you import the SwiftUI and UIKit framework. Then create a struct called PageViewController.

import Foundation
import UIKit
import SwiftUI

struct PageViewController {
    
}

We want to work with our subviews in this file. Therefore, declare an appropriate array for holding them, which will get “filled” with the UIHostingControllers when initialising the PageViewController inside the OnboardingView.

struct PageViewController {
    
    var viewControllers: [UIViewController]
    
}

In this file, we will create our UIPageViewController for embedding it into the OnboardingView. For being able to do that, we must conform it to the UIViewControllerRepresentable protocol.

struct PageViewController: UIViewControllerRepresentable {
    
    //...
    
}

The UIViewControllerRepresentable protocol has three mandatory methods:

  • makeUIViewController: This method is used for creating the UIViewController we want to present.
  • updateUIViewController: This method updates the UIViewController to the latest configuration every time it gets called .
  • makeCoordinator: This method initialises a Coordinator which serves as a kind of a servant for handling delegate and datasource patterns and user inputs. We will talk about this in more detail later.

Let’s start by creating our UIPageViewController by using the makeUIViewController method. Because we want to return a UIPageViewController we have to indicate this behind the arrow like that:

func makeUIViewController(context: Context) -> UIPageViewController {
        
    }

Let’s create the UIPageViewController and return it:

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

        return pageViewController
    }

The updateUIViewController will get called every time the UIPageViewController gets updated, for example when the number of content views of it are changing. Because we use a static number of view controllers for our PageViewController, this function will only get called once, namely when the UIPageViewController is being rendered first. When this happens, we have to configure our PageViewController with setting the first view controller of the viewControllers array to display first. We do this by using the setViewController method.

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

At this point, our PageViewController is ready to be embedded! We can now insert it into our OnboardingView by passing our Subviews. Let’s also add a frame to it.

struct OnboardingView: View {
    
    //...
    
    var body: some View {
        PageViewController(viewControllers: subviews)
            .frame(height: 600)
    }
}

Hint: We will implement the Text objects, the buttons and the Page Control, in the next part of this tutorial.

Great, we successfully implemented an UIPageViewController into a SwiftUI view! But when running the app in live mode, you notice that we are currently not able to swipe back and forth. This is because we didn’t yet implement a datasource pattern for telling our Page View Controller what subview to display when the user swipes back or forth.

Let’s change this by implementing a Coordinator!

Implementing the Coordinator 👷‍♂️

A Coordinator is implemented by creating a subclass inside the PageViewController, which we can then use for implementing the common datasource functions we need for telling our PageViewController which subview to display when the user swipes.

The Coordinator is setup by inserting a Coordinator subclass into the PageViewController with having a parent variable (of the superclass type, in our case that’s PageViewController) and a init function which we need for initialising the Coordinator in our PageViewController.

struct PageViewController: UIViewControllerRepresentable {
    
    //...
    
    class Coordinator: NSObject {
        
        var parent: PageViewController

        init(_ pageViewController: PageViewController) {
            self.parent = pageViewController
        }
        
    }
    
}

We can now initialise the Coordinator by calling the makeCoordinator method of the UIViewControllerRepresentable protocol.

struct PageViewController: UIViewControllerRepresentable {
    
    //...
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIViewController(context: Context) -> UIPageViewController {
        //...
    }
    
    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
        //...
    }
    
    class Coordinator: NSObject {
        
        //...
        
    }
    
}

As said we can use our Coordinator for implementing common Cocoa patterns, like delegates, data source and responding to user input. We want our Coordinator to act as the data source for our UIPageViewController. So let’s conform the Coordinator Subclass to the UIPageViewControllerDataSource protocol.

struct PageViewController: UIViewControllerRepresentable {

//...
    
class Coordinator: NSObject, UIPageViewControllerDataSource {

}

This protocol requires one function for returning the right ViewController after the currently displayed ViewController in our viewControllers array and one for returning the right ViewController before the currently displayed ViewController of our viewControllers array.

So let’s add the viewControllerBefore and viewControllerAfter methods for telling our PageViewController which view controller of the viewControllers array to display when the user swipes.

class Coordinator: NSObject, UIPageViewControllerDataSource {
        
        //...
        
        func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
            //retrieves the index of the currently displayed view controller
            guard let index = parent.viewControllers.firstIndex(of: viewController) else {
                 return nil
             }
            
            //shows the last view controller when the user swipes back from the first view controller
            if index == 0 {
                return parent.viewControllers.last
            }
            
            //show the view controller before the currently displayed view controller
            return parent.viewControllers[index - 1]
            
        }
        
        func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
            //retrieves the index of the currently displayed view controller
            guard let index = parent.viewControllers.firstIndex(of: viewController) else {
                return nil
            }
            //shows the first view controller when the user swipes further from the last view controller
            if index + 1 == parent.viewControllers.count {
                return parent.viewControllers.first
            }
            //show the view controller after the currently displayed view controller
            return parent.viewControllers[index + 1]
        }
    }

Since our Coordinator subclass now conforms to the UIPageViewControllerDataSource protocol, we can assign it as the data source of our PageViewControllers inside our makeViewController method.

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

Great! Now every time the user swipes to a new subview, the Coordinator’s viewControllerAfter/viewControllerBefore method gets called and tells the PageViewController to display the previous or next view controller!

We can check if that’s works by running our app/the OnboardingView preview in live mode and swiping. You see, that every time we swipe, the correct subview get displayed to us!

This is what we’ve accomplished so far:


You can download the source code from GitHub (Part1/App Onboarding folder)

Conclusion 🎊

Great, we just learned how to use a UIPageViewController in SwiftUI! Beside that, we also learned how to feed this controller with SwiftUI views by using UIHostingControllers. In the next article, we will finish our app onboarding, by adding the Texts, setting up the dotted page indicator and implementing a button which the user can use for going to the next view of the PageViewController.

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 👇

3 replies on “How to create an onboarding screen in SwiftUI #1 – Embedding a UIPageViewController”

Howdy! That was an awesome tutorial! It’s a little over my head but I’m trying to absorb as much as I’m able. It was very helpful to have the comments in the code, much appreciated! Learning where and how to choose the right methods and functions in these projects has been the most difficult aspect of learning iOS programming. Why nobody has bothered to break down these processes into bite sized digestible chunks is beyond me. OTOH, I can see extensive opportunity to create tutorials that address the lack of information in these regards. It’s one thing to simply attempt at reading the docs and another to really grasp and understand how specific things such as integrating UIKit and SwiftUI views work, such as this tutorial.

i.e. I understand what the libraries are and how to access them, I understand how to make subclasses and instances of the classes etc. What I don’t understand is how you figure out what classes and methods are needed to put everything together. I have yet to find a tutorial in the past 3 years that really breaks down and addresses these topics in a step by step logical format.

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