Search code examples
iosswiftuipageviewcontrolleruipagecontrol

Swift UIPageView - how to get the index for PageControll?


I am trying to get the pageControl indicator dots working, but I am unable to find out how to get the current viewController to set pageControl.currentPage to the right index.

This is how I set up the UIPageController:

class PageViewController: UIPageViewController, UIPageViewControllerDataSource {

var pageControl = UIPageControl()

lazy var viewControllerList: [UIViewController] = {

    let sb = UIStoryboard(name: "Main", bundle: nil)

    let vc1 = sb.instantiateViewController(withIdentifier: "VC1")
    let vc2 = sb.instantiateViewController(withIdentifier: "VC2")
    let vc3 = sb.instantiateViewController(withIdentifier: "VC3")

    return [vc1, vc2, vc3]
}()

override func viewDidLoad() {
    super.viewDidLoad()

    //  seting up the dots
    pageControl.frame = CGRect(x: view.frame.midX - 50, y: view.frame.maxY - 80, width: 100, height: 100)
    pageControl.numberOfPages = 3
    pageControl.currentPage = 0
    view.addSubview(pageControl)
    view.bringSubview(toFront: pageControl)


    self.dataSource = self

    if let firstViewController = viewControllerList.first {
        self.setViewControllers([firstViewController], direction: .forward, animated: true, completion: nil)
    }
}

func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
    guard let viewControllerIndex = viewControllerList.index(of: viewController) else {return nil}

    let prevIndex = viewControllerIndex - 1
    guard prevIndex >= 0 else {return nil}
    guard viewControllerList.count > prevIndex else {return nil}

    return viewControllerList[prevIndex]

}

func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
    guard let viewControllerIndex = viewControllerList.index(of: viewController) else {return nil}

    let nextIndex = viewControllerIndex + 1
    guard viewControllerList.count != nextIndex else {return nil}
    guard viewControllerList.count > nextIndex else {return nil}

    return viewControllerList[nextIndex]
}
}

I tried a couple of answers on here, but none seems to work in my case. How can I find out what the current index is to set pageControl.currentPage to the right index (I guess I have to set it in both functions viewControllerBefore and viewControllerAfter?

I tried to use viewControllerIndex (previewIndex, or nextIndex) but this lets the dot indicator jump from the first dot the third (not randomly, but not ordered either).


Solution

  • UIPageViewController has a built-in page indicator that appears if the required UIPageViewControllerDataSource methods are implemented. From the documentation:

    If both of the methods in Supporting a Page Indicator are implemented and the page view controller’s transition style is scroll, a page indicator is visible.

    The required methods are:

    public func presentationCount(for pageViewController: UIPageViewController) -> Int
    public func presentationIndex(for pageViewController: UIPageViewController) -> Int
    

    But your example uses a custom UIPageControl instead of the built-in page indicator. That approach offers more flexibility. For example, while the built-in control is always at the bottom of the view, you can easily position your custom control at the top of the view.

    To connect your custom UIPageControl to the UIPageViewController you must implement UIPageViewControllerDelegate. This allows setting pageControl.currentPage in response to a swipe gesture. To do so:

    Add a member to your class:

    var pendingPage: Int?
    

    Set the delegate:

    override func viewDidLoad() {
        ...
        self.delegate = self
        ...
    }
    

    And implement some methods of the delegate protocol. I prefer to add an extension for each protocol so that the protocol name and its implementation are clearly grouped.

    extension PageViewController: UIPageViewControllerDelegate { 
        // Sent when a gesture-initiated transition begins.
        func pageViewController(_ pageViewController: UIPageViewController,
                                willTransitionTo pendingViewControllers: [UIViewController]) {
            pendingPage = viewControllerList.index(of: pendingViewControllers.first!)
        }
    
        // Sent when a gesture-initiated transition ends. The 'finished' parameter indicates whether the animation finished,
        // while the 'completed' parameter indicates whether the transition completed or bailed out (if the user let go early).
        func pageViewController(_ pageViewController: UIPageViewController,
                                didFinishAnimating finished: Bool,
                                previousViewControllers: [UIViewController],
                                transitionCompleted completed: Bool) {
            guard completed, let page = pendingPage else {
                return
            }
            pageControl.currentPage = page
        }
    }