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:

segment control navigating a page view controller

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
}
  1. The Constants enum is a namespace. It stores our static model.
  2. The viewControllers property zips the label titles and colors together and runs the output through our viewController function.
  3. 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)
}

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:

segment control navigating a page view controller

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.