Doing software development in most of the applications there is some kind of onboarding screens that simply explains the basic functionality of the given app. Since SwiftUI is with us for some time and it is not supporting this kind of view let me show you how to implement UIPageViewController behaviour using this amazing framework!

Unsplash.com

This article can be found on my blog page here! (where medium doesn’t truncat gists!)

Project setup

File new project

So…. yeah, I love Swift Playgrounds, but in this case, Playgrounds are not working well with SwiftUI and DragGesture recognizers, so this article will be implemented inside new SwiftUI project.

SwiftUI?

Going with SwiftUI definition it is a framework that allows building user interfaces in a declarative way. But what that even mean?

A declarative way of thinking implicates that you don’t have to care to tell the app about the state updates. Wait… what?

Let’s assume you would like to show an e-mail composer. In an imperative way of thinking, we will be forced to declare an e-mail composer view and whenever we would like to show it we need to call some function that will present this view. Nevertheless, the declarative way allows defining some property that will be observed by the e-mail composer view whatever this view should be shown or not. The most important thing here, you need to declare this property once and don’t have to care about moving between states anymore!

So yeah, let’s just start!

PageView

PageView confirms to View protocol, which means that it will implement the property of `some View` type.

some keyword here defines opaque result type, it was introduced in Swift 5.1 and you can read more about it here https://docs.swift.org/swift-book/LanguageGuide/OpaqueTypes.html

Inside view property we need to define:

`Image` corresponds with UIImage type and it displays just an image. This image has two builder functions applied:

resizable() — this tells image to fit the screen

clipped() — this tells image that it shouldn’t screech outside the parent view

And that’s it.

PageViewData confirms to Identifiable protocol. You can read more about it here: https://nshipster.com/identifiable/

In general, identifiable allows the SwiftUI engine to optimize the memory by identifying if the given view needs to be created from scratch or if the SwiftUI engine can reuse already created View.

CircleButton

The second element of this puzzle is CircleButton which is a little more advanced View then PageView. CircleButton will indicate which page is currently displayed and allows the user to navigate between pages.

So let’s dived CircleButton into first parts:

@Binding var isSelected: Bool

Whooow, whooow, @Binding var… what is that. Let me explain. @Binding is a property wrapper, property what…? Property wrapper is a generic data structure that allows you to change the way that property can be read or write. Here Binding means that this property can be changed dynamically, and what is more SwiftUI will observe any changes of this property. Let’s see how it’s done.

Circle().foregroundColor(self.isSelected ? Color.white : Color.white.opacity(0.5))

The foreground color of this Circle view will be updated whenever self.isSelected property will be changed! SwiftUI rocks, yeah?

let action is just swift callback that will be executed whenever the user clicks this button.

SwiperView

This view is a little more complicated than other views, but there is nothing that you should be afraid of. Let’s start with declaring the required properties:

Properties:

let pages: [PageViewData]

@Binding var index: Int

@State private var offset: CGFloat = 0

@State private var isUserSwiping: Bool = false

So, there is one new property wrapper introduced in this view, it’s called @State is a wrapper for values on which view will base its current state. @State property should be edited only by the view itself selfs so that’s why Apple recommends to always declare the state as private property. You can check more about @State property wrapper here:

https://developer.apple.com/documentation/swiftui/state.

pages represent a collection of view data elements that allow as to create the corresponding view

@Binding var index: Int represents the current index of PageView displayed on a screen

@State var offset: CGFloat = 0 represents the current offset of a finger on the screen while the user is swiping on the screen

@State private var isUserSwiping: Bool allows SwiftUI to indicate whatever there is a swipe action going on, or swipe action is ended.

And right now we can declare body inside the SwiperView

Inside body we have declared:

GeometryReader — this object is nothing more than a quasi-view that allows reading current container geometry. You can check more about this object here: https://swiftui-lab.com/geometryreader-to-the-rescue/

ScrollView — this is SwiftUI object that directly corresponds with UIScrollView and allows to change offset of it content. (Which we will do)

HStack — horizontal stack view.

ForEach(self.pages) — this special kind of loop allows enumerating objects inside body property. It’s used to create our PageView objects.

ScrollView content.offset updates:

As we would like to scroll PageViews inside our scrollView we need to control content offset property of ScrollView. To do that we will use some SwiftUI magic.

content.offset is a builder function that allows indicating current offset for views alignment inside the scroll view.

offset is calculated based on isUserSwipping boolean. Whenever it’s true, offset is read from DragGesture.onChanged callback, when the user is not swapping offset is equal to the current page index multiplied by screen width.

.frame defines that scrollView should fit the screen, and on load, views should be leading alignment. (without this line, our scrollView will display middle PageView on load)

.gesture() method allows adding gesture recognizers for this view.

DragGesture

onChanged closure is invoked while the user is swiping the screen, then we will assign isUserSwiping as true, and calculate content offset basing on `value.translation.width` property

onEnded here we need to calculate if PageView should be moved to the left or the right and page on which index should be displayed next.

withAnimation { } tells SwiftUI that any changes inside this closure should be animated

ContentView

ContentView contains:

pages — an array of PageViewData objects

@State private var index: Int = 0 — which indicate which PageView is currently selected

Inside body we have declared:

ZStack — View that allows aligning its children in this same axis (the second view is always on top of the first view)

SwiperView — view that we have just created and it displays PageViews

HStack — This stack view will be used to display Page View navigation buttons

ForEach(0..<self.pages.count) { index in … } that allows us to create navigation buttons

padding which defines bottom padding for HStack view

And Boom, the effect is magnificent!

Thanks for ready! Full code can be found here!