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 view controlllers 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.
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.
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:
func makeCoordinator() -> PageViewController.Coordinator {
Coordinator(self)
}
This method needs to be called exactly like that and its job is to instantiate the coordinator.
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.")
]
}
}
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
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:
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.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!!!