Onboarding Screen

SwiftUI Dec 10, 2019

We all know how awesome swiftUI is, but as great as it is, it's still young and missing some important features. So from time to time you may need to tap into the great UIKit framework to use some of its amazing features. Likely for us, there's a way of integrating UIKit views and viewControlllers in SwiftUI. The process is a little bit

verbose but it gets the job done.

In this article, I will show you how to create an onboarding screen using UIPageViewController which is a UIKit feature. I will also follow-up this tutorial with a login and sign up screens, so make to subscribe to get notified when the article comes out.

Here is what we are going to build:

Download the completed project here

View Controller

Create a folder named ViewControllers and add a swift file named PageViewController.swift inside containing the following code:


struct PageViewController: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIPageViewController 
func updateUIViewController(_ pageViewController: UIPageViewController, context: UIViewControllerRepresentableContext<PageViewController>) 
}

To use the PageViewController in swiftUI, it needs to conform to the UIViewControllerRepresentable protocol which will require you to override 2 methods which I explain below.

makeUIViewController: This is where you will initialise the controller that you want to use in your swiftUI app and return it. In our case we are returning a UIPageViewController type.

•   updateUIViewController: This is where you can make any update to your controller once it has been created in makeUIViewController.

Those 2 methods are not the only ones that you can override, we will override on more methods and the fourth one is not needed in this tutorial.

Let's now update the first 2 methods, but before that, add the following properties at the top of the file.

var controllers: [UIViewController]
@Binding var currentPage: Int

In the makeUIViewController methods, add the following code:


let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
 controllers.forEach({$0.view.backgroundColor = UIColor(named: "bgColor") })
pageViewController.view.backgroundColor = UIColor(named: "bgColor")
return pageViewController

Like I said earlier, we will make all the initial setups in the make methods. The above code does the following:

• We initialise the pageViewControlller

• We set the datasource and delegate to the context's coordinator which we will create shortly

• And last, I just set background colours to all the pages.

The in the updateUIViewController, add the following line of code:

pageViewController.setViewControllers([controllers[currentPage]], direction: .forward, animated: true)

In the above code block, I update the page controller to show the current page after the user has scrolled.

Coordinator

The coordinator will be the class that will coordinate, as its name implies, the interaction between swiftUI with whatever controller we are using. This is the class that will handle all the datasource and delegate conformance, that's why we set the page controller's delegate and datasource to the parent coordinator.

Now create a new class inside the PageController struct and name it Coordinator like this:

 class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
        
        var parent: PageViewController
        
        init(_ controller: PageViewController) {
            self.parent = controller
        }
        
        func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
            guard let index  = parent.controllers.firstIndex(of: viewController) else {
                return nil
            }
            
            if index == 0 {
                return nil
            }
            return parent.controllers[index - 1]
        }
        
        func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
            guard let index  = parent.controllers.firstIndex(of: viewController) else {
                return nil
            }
            
            if index == parent.controllers.count - 1{
                return nil
            }
                      
            return parent.controllers[index + 1]
        }
        
        func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
            if completed,
                let visibleViewController = pageViewController.viewControllers?.first,
                let index = parent.controllers.firstIndex(of: visibleViewController) {
                parent.currentPage = index
            }
        }
    }

This code is pretty long, so let’s break it down:

  1. We first conform the Coordinator to the page datasource and delegate like a normal UIViewController
  2. We create a property that will keep a reference to the parent struct that will be set when the coordinator is instantiated.
  3. We then implement the first 2 datasource methods to tell the controller which page to show from the parent’s controllers array
  4. And last, in the delegate method we capture the index that the user has scrolled to and set it the parent’s currentPage which is a Binding property that will be used to update the user interface in SwiftUI.

You now need to add the following code in the parent struct:

    func makeCoordinator() -> PageViewController.Coordinator {
        Coordinator(self)
    }
    

This method needs to be called exactly like that and its job is to instantiate the coordinator.

Models

Create a Models folder with a file name Page.swift inside containing the following code:


import Foundation

struct Page: Identifiable {
    
    let id: UUID
    let image: String
    let heading: String
    let subSubheading: String
    
    static var getAll: [Page] {
        [
            Page(id: UUID(), image: "screen-1", heading: "Form new habits", subSubheading: "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna."),
            Page(id: UUID(), image: "screen-2", heading: "Keep track of your progress", subSubheading: "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna."),
            Page(id: UUID(), image: "screen-3", heading: "Setup your goals", subSubheading: "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna."),
            Page(id: UUID(), image: "screen-4", heading: "Keep track of your progress", subSubheading: "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna.")
            
        ]
    }
}

Page View

Let’s now create the page view that will be contained in a controller. Before you start, make sure you’ve downloaded the completed project and copy all images and colours into your project.

Create a Views folder and inside it add a swiftUI file named PageView.swift with the following content inside:


import SwiftUI

struct PageView: View {
    
    var page = Page.getAll.first!
    
    var body: some View {
            VStack{
                
                Image("screen-1")
                VStack{
                    Text(page.heading).font(.title).bold().layoutPriority(1).multilineTextAlignment(.center)
                    Text(page.subSubheading)
                        .multilineTextAlignment(.center)
                }.padding()
            }
        
    }
}

struct PageView_Previews: PreviewProvider {
    static var previews: some View {
        PageView()
    }
}

This code is simple, it just creates views stacked horizontally.

Preview

Swift UI App
Swift UI App

Page View Container

This is the View that will package page views and send them to the PageViewController and we will make it generic so that you can reuse it however you want.

Now add the following code in the file:

import SwiftUI

struct PageViewContainer<T: View>  : View {
    
    var viewControllers: [UIHostingController<T>]
    @State var currentPage = 0
    
    var body: some View {
        
        return VStack {
            PageViewController(controllers: viewControllers, currentPage: self.$currentPage)
            
            PageIndicator(currentIndex: self.currentPage)
            
            LCButton(text: currentPage == viewControllers.count - 1 ? "Get started" : "Next") {
                if self.currentPage < self.viewControllers.count - 1{
                    self.currentPage += 1
                }
            }.padding()
            
        }.background(Color.backgroundColor)
    }
}

struct PageViewContainer_Previews: PreviewProvider {
    static var previews: some View {
        PageViewContainer(viewControllers: Page.getAll.map({  UIHostingController(rootView: PageView(page: $0) )  }))
    }
}


Let’s go through this code step by step:

  1. We add an array of HostingControllers property because in order to use a swiftUI view in UIKit, we need to create a HostingController instance with it. So this will be an array of UIHostingControllers where their rootViews are PageView instances.
  2. Then we add the PageViewController , the PageIndicator that we still need to create and a next button that we will also create shortly.

In the Views folder, add the a file named PageIndicator.swift containing the following code:

struct PageIndicator: View {
    
    var currentIndex: Int = 0
    var pagesCount: Int = 4
    var onColor: Color = Color.accentColor
    var offColor: Color = Color.offColor
    var diameter: CGFloat = 10
    
    var body: some View {
        HStack{
            ForEach(0..<pagesCount){ i in
                Image(systemName: "circle.fill").resizable()
                    .foregroundColor( i == self.currentIndex ? self.onColor : self.offColor)
                    .frame(width: self.diameter, height: self.diameter)

            }
        }
    }
}

struct PageIndicator_Previews: PreviewProvider {
    static var previews: some View {
        PageIndicator()
    }
}

Like all views I create, I make sure you can customise colours and size of this view. Beside that, the code is pretty simple

Preview

In the same folder, add a file named LCButton.swift with the following code inside:


import SwiftUI

struct LCButton: View {
    var text = "Next"
    var action: (()->()) = {}
    
    var body: some View {
      Button(action: {
        self.action()
      }) {
        HStack {
            Text(text)
                .bold()
                .frame(minWidth: 0, maxWidth: .infinity)
                .padding(.vertical)
                .accentColor(Color.white)
                .background(Color("accentColor"))
                .cornerRadius(30)
            }
        }
    }
}

struct LCButton_Previews: PreviewProvider {
    static var previews: some View {
        LCButton()
    }
}

With that out of the way, if you go and resume the PageViewContainer preview, you should get this:

Now put this in the contentView’s body and you should be able to run the app and scroll or click the button to cycle through the pages.

        VStack {
            PageViewContainer( viewControllers: Page.getAll.map({  UIHostingController(rootView: PageView(page: $0) ) }))
        }.frame(maxHeight: .infinity).background(Color.backgroundColor).edgesIgnoringSafeArea(.all)           

You can also switch to dark mode and should still look awesome.

Like this tutorial? Share it… Lets make the community grow, I have lots of stuff in store. Stay tuned for the follow-up tutorial. HAPPY CODING!!!

John K

I am a software developer and code enthusiast. Do you want to work with me, have a suggestion or a request? Feel free to contact me at [email protected] or https://twitter.com/liquidcoder