The purpose of this project is to teach you how to create a scrolling view that displays a couple of different view controllers and also contains static buttons that stay in place when the view controllers transition from one to the other. We will focus this project on creating a user authentication flow.
There are a couple of ways this can be achieved, each having advantages and disadvantages. We could use a UIPageViewController
, UIScrollView
or UINavigationViewController
. However, in most cases the easiest solution is also the best one so we'll go with the one I find easiest, and that's using a UINavigationViewController
.
The previous two require some work in order to go from one screen to the other. In the case of UIScrollView
we need to write some functions that manually scroll the views in place and in the case of UIPageViewController
we have to mess with its dataSource
a little and set the next view controller using the setViewControllers
method. The latter is probably not that bad, but I prefer using the UINavigationViewController
method.
We're going to have a container view
in for the top part which will hold all other view controllers in our authentication flow. In the bottom part we're going to have two buttons that control the movement of the views above.
Embedded in the container view
we have the UINavigationController
which will, in turn hold all the other views. Tapping Next
will push the next view controller to the navigation stack and tapping Previous
will pop the current view controller.
First the easy parts, let's add the two buttons on the bottom part.
- Add two buttons and embed them in a vertical
UIStackView
. Set the title of the top oneNext
and the bottom onePrevious
. Then addleading
,trailing
andbottom
constraints with constants of20
. That's all, our buttons are done. - Now add a
Container View
and constraint it to all four edges with a constant of20
again. I guess you can add a little more space on the bottom constraint. - Click on the newly created view controller (the one that's embedded in the container view) and go to
Editor -> Embed In -> Navigation Controller
. - Now for the last touch, click the
Navigation Bar
in theUINavigationController
and thickPrefers Large Titles
.
That's our first view controller covered.
Now we're going to create the necessary view controllers for the authentication process.
- Add a new
UIViewController
in theStoryboard
, then add aUILabel
inside of it and twoUITextFields
. Set theUILabel's
text to "What's your name" and set the placeholders for theUITextFields
to "First Name" and "Last Name". - Select the two
UITextFields
and embed them in a verticalUIStackView
with a spacing of10
. - Select the previously created
UIStackView
and andUILabel
and embed them in another verticalUIStackView
with a higher spacing, of40
. - Now set the
UIViewController's
title to "Let's get started". And that's the UI for the first view controller in the flow done. - Click the previously created view controller and tap
CMD + D
to duplicate it, set theUILabel's
text to "What's your email", remove one of theUITextFields
and set the placeholder of the remaining one to "Email". Set theUIViewController's
title to "Hi John" and that's it. - Now duplicate the last
UIViewController
and replace theUILabel's
text with "What's your phone number?" and the placeholder text in theUITextField
to "Phone Number". Change theUIViewController's
title to "Just one more thing" and the UI stuff is done.
Now let's get into some coding. We'll start by creating UIViewController
classes for all of our authentication view controllers.
- First of all, create a new folder called "AuthViewControllers"
- Press
CMD + N
, type "cocoa" and pickCocoa Touch Class
, then type "NameViewController". - Repeat the previous step to create another two classes called "EmailViewController" and "PhoneViewController".
- Put them all in the "AuthViewControllers" folder to keep things tidy.
- Now create another
Cocoa Touch Class
called "ContainerViewController". This class will hold and orchestrate all of the auth view controllers. - One last step. Go to the
Main.storyboard
again and in theIdentity Inspector
set the custom classes that we've created for everyUIVIewController
.
Now we'll add some code in each class that we've created. Let's start with the existing "ViewController".
- We need to be able to communicate with the container view so let's create a property that references it.
private var containerViewController: ContainerViewController? {
((self.children.first as? UINavigationController)?.viewControllers.first as? ContainerViewController)
}
- What this code does is it takes the first
ChildViewController
, cast it toUINavigationController
, then take it's first view controller and cast it toContainerViewController
which is our custom class. - Create some
@IBActions
for the button taps and that's all
@IBAction func primaryButtonTapped(_ sender: Any) { }
@IBAction func secondaryButtonTapped(_ sender: Any) { }
We have to do more work here, in the "ContainerViewController". Let's start by creating an Enum
to hold our Auth View Controllers. You can define it inside the "ContainerViewController".
enum AuthViewControllers {
case name
case email
case phone
}
- Now add a property to hold the current view controller
private var currentViewController: AuthViewControllers = .name
- Before we go further, let's make our lives easier and add a handy extension to make it easier to load
UIViewControllers
fromUIStoryboard
. Create a new class, you can call itUIStoryboard+Extensions.swift
and add these lines:
extension UIStoryboard {
func instantiateViewController<T>(ofType type: T.Type) -> T {
let identifier = String(describing: type)
guard let viewController = instantiateViewController(withIdentifier: identifier) as? T else {
fatalError("Cannot instantiate view controller")
}
return viewController
}
}
extension UIStoryboard {
static let main = UIStoryboard(name: "Main", bundle: nil)
}
- Because Swift enums are so cool, we can write additional code in our enum to aid us in retrieving the right
UIViewController
. So add the following functions to the "AuthViewControllers" enum:
func initial() -> UIViewController {
switch self {
case .name:
return UIStoryboard.main.instantiateViewController(ofType: NameViewController.self)
case .email:
return UIStoryboard.main.instantiateViewController(ofType: EmailViewController.self)
case .phone:
return UIStoryboard.main.instantiateViewController(ofType: PhoneViewController.self)
}
}
mutating func next() -> UIViewController {
switch self {
case .name:
self = .email
return UIStoryboard.main.instantiateViewController(ofType: EmailViewController.self)
case .email:
self = .phone
return UIStoryboard.main.instantiateViewController(ofType: PhoneViewController.self)
case .phone:
self = .name
return UIStoryboard.main.instantiateViewController(ofType: NameViewController.self)
}
}
- Notice the
mutating
before thenext()
func. It means that inside the function we're also modifying the current instance of the enum. That's really convenient since we don't have to do it in theContainerViewController
after we call the function. - Now we can call the
initial()
function in theContainerViewController
, so put this line of code in theviewWillAppear
function:
navigationController?.pushViewController(currentViewController.initial(), animated: false)
- The last thing we need to add is some functions to push and pop view controllers
func goToNextViewController() {
navigationController?.pushViewController(currentViewController.next())
}
func goToPreviousViewController() {
navigationController?.popViewController(animated: true)
}
Now that we have everything in place, we can complete the actions when tapping the buttons:
@IBAction func primaryButtonTapped(_ sender: Any) {
containerViewController?.goToNextViewController()
}
@IBAction func secondaryButtonTapped(_ sender: Any) {
containerViewController?.goToPreviousViewController()
}
Now you can finally run the app and hopefully everything works just fine. You should be able to go back and forth between those auth view controllers.
But you might have noticed that the result is not very visually appealing. That default push and pop navigation transition doesn't look good at all when embedded in a Container View
. So how can we fix this? Create a new class called UINavigationController+Extensions.swift
and add the following lines:
public extension UINavigationController {
func pop(transitionType type: CATransitionType, subtype: CATransitionSubtype, duration: CFTimeInterval = 0.3) {
self.addTransition(transitionType: type, subtype: subtype, duration: duration)
self.popViewController(animated: false)
}
func push(viewController: UIViewController, transitionType type: CATransitionType, subtype: CATransitionSubtype, duration: CFTimeInterval = 0.3) {
self.addTransition(transitionType: type, subtype: subtype, duration: duration)
self.pushViewController(viewController, animated: false)
}
private func addTransition(transitionType type: CATransitionType, subtype: CATransitionSubtype, duration: CFTimeInterval = 0.3) {
let transition = CATransition()
transition.duration = duration
transition.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
transition.type = type
transition.subtype = subtype
self.view.layer.add(transition, forKey: nil)
}
}
- This
extension
adds the ability add custom transitions on theUINavigationController
- Now we can go back in the
ContainerViewController
and replace the contents of thegoToNextViewController()
andgoToPreviousViewController()
functions with:
func goToNextViewController() {
navigationController?.push(viewController: currentViewController.next(), transitionType: .push, subtype: .fromRight)
}
func goToPreviousViewController() {
navigationController?.pop(transitionType: .push, subtype: .fromLeft)
}
Notice that we didn't do anything about the Back
button in the UINavigationViewController
. We definitely shouldn't be able to see and interact with it. To fix this, we need to go into every one of the auth view controllers and add this line in the viewDidLoad()
method:
navigationItem.setHidesBackButton(true, animated: false)
That was it. I hope that you've enjoyed it and that you've at least learnt a few new tricks.