UIPageViewController
iOS development
view management
Swift programming
mobile app design

UIPageViewController return the current visible view

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

UIPageViewController is a solid choice for swipe-based onboarding, galleries, and step flows, but many bugs come from incorrect current-page tracking. The visible view controller can change during gestures, canceled swipes, and programmatic navigation. A correct implementation treats transition completion as the single source of truth.

Build a Stable Paging Model

Start with a persistent array of page controllers. Do not recreate controllers every time the data source asks for before or after pages. Identity stability makes index lookup reliable.

swift
1import UIKit
2
3final class PagerHostViewController: UIViewController {
4    private lazy var pageViewController: UIPageViewController = {
5        let vc = UIPageViewController(
6            transitionStyle: .scroll,
7            navigationOrientation: .horizontal
8        )
9        vc.dataSource = self
10        vc.delegate = self
11        return vc
12    }()
13
14    private lazy var pages: [UIViewController] = [
15        FirstPageViewController(),
16        SecondPageViewController(),
17        ThirdPageViewController()
18    ]
19
20    private(set) var currentIndex: Int = 0
21
22    override func viewDidLoad() {
23        super.viewDidLoad()
24
25        addChild(pageViewController)
26        view.addSubview(pageViewController.view)
27        pageViewController.view.frame = view.bounds
28        pageViewController.didMove(toParent: self)
29
30        pageViewController.setViewControllers(
31            [pages[0]],
32            direction: .forward,
33            animated: false
34        )
35    }
36}

This setup guarantees that page identity and order remain deterministic.

Read the Currently Visible Page Safely

When the page view controller is idle, the current visible controller is viewControllers?.first. This is useful for immediate checks, but avoid calling it before initial setup.

swift
1extension PagerHostViewController {
2    var visiblePage: UIViewController? {
3        return pageViewController.viewControllers?.first
4    }
5}

If you need the current index, resolve it from the same pages array:

swift
1extension PagerHostViewController {
2    var visibleIndex: Int? {
3        guard let vc = visiblePage else { return nil }
4        return pages.firstIndex(of: vc)
5    }
6}

Keep this as a read helper. Do not mutate state from it during active transition callbacks.

Update State Only When a Transition Finishes

The most important callback is didFinishAnimating. Update index only when transitionCompleted is true. That avoids incorrect state when a swipe starts but snaps back.

swift
1extension PagerHostViewController: UIPageViewControllerDelegate {
2    func pageViewController(
3        _ pageViewController: UIPageViewController,
4        didFinishAnimating finished: Bool,
5        previousViewControllers: [UIViewController],
6        transitionCompleted completed: Bool
7    ) {
8        guard completed,
9              let visible = pageViewController.viewControllers?.first,
10              let index = pages.firstIndex(of: visible)
11        else {
12            return
13        }
14
15        currentIndex = index
16        print("Current page index: \(currentIndex)")
17    }
18}

This one rule prevents most page indicator and analytics drift.

Keep Data Source Logic Symmetric

Your data source methods must use the same array and indexing rules as your tracking logic. If one method uses a filtered list or a recreated controller, the visible index can become invalid.

swift
1extension PagerHostViewController: UIPageViewControllerDataSource {
2    func pageViewController(
3        _ pageViewController: UIPageViewController,
4        viewControllerBefore viewController: UIViewController
5    ) -> UIViewController? {
6        guard let index = pages.firstIndex(of: viewController), index > 0 else {
7            return nil
8        }
9        return pages[index - 1]
10    }
11
12    func pageViewController(
13        _ pageViewController: UIPageViewController,
14        viewControllerAfter viewController: UIViewController
15    ) -> UIViewController? {
16        guard let index = pages.firstIndex(of: viewController), index < pages.count - 1 else {
17            return nil
18        }
19        return pages[index + 1]
20    }
21}

If you later make pages dynamic, update all related logic together.

Programmatic Navigation Without State Drift

Many apps have a next button in addition to swipe gestures. Programmatic navigation should update currentIndex from completion, not from the requested target alone.

swift
1extension PagerHostViewController {
2    func navigate(to target: Int, animated: Bool = true) {
3        guard pages.indices.contains(target), target != currentIndex else { return }
4
5        let direction: UIPageViewController.NavigationDirection =
6            target > currentIndex ? .forward : .reverse
7
8        pageViewController.setViewControllers(
9            [pages[target]],
10            direction: direction,
11            animated: animated
12        ) { [weak self] finished in
13            guard let self = self else { return }
14            if finished || !animated {
15                self.currentIndex = target
16            }
17        }
18    }
19}

This keeps gesture-driven and button-driven behavior consistent.

Connect Page Tracking to UI Elements

If you use UIPageControl, update it in one place, typically after successful transition completion. The same applies to analytics and event logging. One update path avoids duplicate events and contradictory state.

A practical pattern is to create a small onPageChanged(index:) method and call it from both gesture completion and programmatic completion.

Common Pitfalls

  • Recreating page controllers in data source methods, which breaks identity checks and index lookup.
  • Updating index during gesture start instead of completion, causing wrong state after canceled swipes.
  • Reading viewControllers?.first before initial setViewControllers call, which returns no useful value.
  • Maintaining separate order arrays for data source and UI logic, leading to mismatch.
  • Setting currentIndex immediately on button tap without waiting for programmatic transition completion.

Summary

  • Keep a stable pages array and reuse those controller instances.
  • Treat transition completion as the authoritative moment for index changes.
  • Resolve visible page index from controller identity, not assumptions.
  • Use shared logic for swipe and button navigation state updates.
  • Centralize UI and analytics page-change handling to avoid duplicate or stale events.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.