Let’s learn how to navigate a UIPageViewController
using a UISegmentedControl
in iOS. The UIPageViewController
provides a way to display pages of content, where each page is managed by its own view controller. The UISegmentedControl
displays segments and allows users to switch between them.
We’ll begin by setting up the UISegmentedControl
and UIPageViewController
, then move on to connecting them by programmatically navigating the UIPageViewController
based on the selection in the UISegmentedControl
.
The result will look something like this:
Making a Model for the Views
Our model consists of an array of titles and an array of colours. The titles will be used to display the segments of the UISegmentedControl
and the colours will be used to display the background colour of each page in the UIPageViewController
.
Here’s how it looks:
final class ViewController {
private enum Constants { // 1
static let titles = ["One", "Two", "Three", "Four"]
static let colors: [UIColor] = [.white, .yellow, .cyan, .orange]
}
private let viewControllers = zip(Constants.titles, Constants.colors).map(viewController) // 2
}
func viewController(_ text: String, _ backgroundColor: UIColor) -> UIViewController { // 3
let viewController = UIViewController()
viewController.view.backgroundColor = backgroundColor
let label = UILabel()
viewController.view.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.text = text
label.centerXAnchor.constraint(equalTo: viewController.view.centerXAnchor).isActive = true
label.centerYAnchor.constraint(equalTo: viewController.view.centerYAnchor).isActive = true
return viewController
}
- The
Constants
enum is a namespace. It stores our static model. - The
viewControllers
property zips the label titles and colors together and runs the output through ourviewController
function. - The
func viewController
is a factory method to create simple view controllers with labels
Adding the UISegmentedControl
The UISegmentedControl
is initialized using the titles defined in the model. The selected segment index is set to 0, which corresponds to the first title. The UISegmentedControl
is added to the view hierarchy of the view controller and its constraints are set up.
private let segmentedControl = UISegmentedControl(items: Constants.titles)
private func setupSegmentedControl() {
view.addSubview(segmentedControl)
segmentedControl.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
segmentedControl.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
segmentedControl.leadingAnchor.constraint(equalTo: view.leadingAnchor),
segmentedControl.trailingAnchor.constraint(equalTo: view.trailingAnchor),
segmentedControl.heightAnchor.constraint(equalToConstant: 44.0)
])
segmentedControl.selectedSegmentIndex = 0
segmentedControl.addTarget(self, action: #selector(didSelect), for: .valueChanged)
}
The didSelect
method is called when the value of the segmented control changes. This method is responsible for navigating the UIPageViewController to the corresponding page. We will implement it shortly.
Adding the UIPageViewController
The UIPageViewController
is initialized using the scroll transition style and the horizontal navigation orientation. This will make it look like we are sliding from one view controller to another. The UIPageViewController
is added as a child view controller and its constraints are set up. We also set the first view controller for the page view controller.
private func setupPageViewController() {
addChild(pageViewController)
view.addSubview(pageViewController.view)
pageViewController.didMove(toParent: self)
pageViewController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
pageViewController.view.topAnchor.constraint(equalTo: segmentedControl.bottomAnchor),
pageViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
pageViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
pageViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
pageViewController.setViewControllers([viewControllers[0]],
direction: .forward,
animated: false)
}
Navigating using the UISegmentedControl tap and the UIPageViewController
Next, we implement the didSelect
function like so:
@objc func didSelect() {
guard let visibleViewController = pageViewController.viewControllers?.first,
let currentIndex = viewControllers.firstIndex(of: visibleViewController),
segmentedControl.selectedSegmentIndex != currentIndex
else {
return
}
let newIndex = segmentedControl.selectedSegmentIndex
let direction: UIPageViewController.NavigationDirection = currentIndex < newIndex ? .forward : .reverse
pageViewController.setViewControllers([viewControllers[newIndex]],
direction: direction,
animated: true)
}
didSelect
is called when the value of the segmented control changes.
The function starts by using a guard
statement to unwrap and assign the first UIViewController
object stored in the pageViewController.viewControllers
array to the visibleViewController
constant. It then finds the index of the current visibleViewController
in the viewControllers
array and assigns it to the currentIndex
constant.
The guard
statement also checks that the segmentedControl.selectedSegmentIndex
is not equal to the currentIndex
. If the statement evaluates to false, the function returns early and does nothing.
If the guard statement evaluates to true, the function continues by calculating the new index for the page view by using the segmentedControl.selectedSegmentIndex
. The direction of the page view transition is then determined based on whether the currentIndex
is less than the newIndex
and set to .forward
or .reverse
accordingly.
Finally, the function updates the page view by calling pageViewController.setViewControllers
and passing in the viewControllers[newIndex]
as the new view controller to display, the calculated direction of the transition and animated set to true.
Putting it all together
All put together the final code should look like this.
import UIKit
final class ViewController: UIViewController {
private enum Constants {
static let titles = ["One", "Two", "Three", "Four"]
static let colors: [UIColor] = [.white, .yellow, .cyan, .orange]
}
private let segmentedControl = UISegmentedControl(items: Constants.titles)
private let pageViewController = UIPageViewController(transitionStyle: .scroll,
navigationOrientation: .horizontal)
private let viewControllers = zip(Constants.titles, Constants.colors).map(viewController)
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupSegmentedControl()
setupPageViewController()
}
private func setupSegmentedControl() {
view.addSubview(segmentedControl)
segmentedControl.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
segmentedControl.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
segmentedControl.leadingAnchor.constraint(equalTo: view.leadingAnchor),
segmentedControl.trailingAnchor.constraint(equalTo: view.trailingAnchor),
segmentedControl.heightAnchor.constraint(equalToConstant: 44.0)
])
segmentedControl.selectedSegmentIndex = 0
segmentedControl.addTarget(self, action: #selector(didSelect), for: .valueChanged)
}
private func setupPageViewController() {
addChild(pageViewController)
view.addSubview(pageViewController.view)
pageViewController.didMove(toParent: self)
pageViewController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
pageViewController.view.topAnchor.constraint(equalTo: segmentedControl.bottomAnchor),
pageViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
pageViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
pageViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
pageViewController.setViewControllers([viewControllers[0]],
direction: .forward,
animated: false)
}
@objc func didSelect() {
guard let visibleViewController = pageViewController.viewControllers?.first,
let currentIndex = viewControllers.firstIndex(of: visibleViewController),
segmentedControl.selectedSegmentIndex != currentIndex
else {
return
}
let newIndex = segmentedControl.selectedSegmentIndex
let direction: UIPageViewController.NavigationDirection = currentIndex < newIndex ? .forward : .reverse
pageViewController.setViewControllers([viewControllers[newIndex]],
direction: direction,
animated: true)
}
}
func viewController(_ text: String, _ backgroundColor: UIColor) -> UIViewController {
let viewController = UIViewController()
viewController.view.backgroundColor = backgroundColor
let label = UILabel()
viewController.view.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.text = text
label.centerXAnchor.constraint(equalTo: viewController.view.centerXAnchor).isActive = true
label.centerYAnchor.constraint(equalTo: viewController.view.centerYAnchor).isActive = true
return viewController
}
And that should give you something like:
Conclusion
Using a UISegmentedControl
or a similar tab-style view to handle navigation through separate view controllers is a common iOS pattern. Hopefully, this post gives a clean and simple example of how UISegmentedControl
can navigate a UIPageViewController
.