How to fix UIPageViewController manual pagination?

Viewed 637

UIPageViewController manual pagination not working properly.

Automatic pagination working fine, but when we rotate manually it's not working properly.

My ViewController.Swift code

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var contentView: UIView!
    @IBOutlet weak var pageControl: UIPageControl!
    let myDataSource = ["Img1", "Img2", "Img3", "Img4"]
    let urlDataSource = ["https://fabric.io/", "https://about.gitlab.com/", "https://developer.apple.com/", "https://fast.com/"]
    var currentVCIndex = 0
    var timer = Timer()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        configurePageViewController()
        timerStart()
    }

    func timerStart() {
        timer.invalidate()
        timer = Timer.scheduledTimer(timeInterval: 5.0, target: self, selector: #selector(configurePageViewController), userInfo: nil, repeats: true)
    }
    
    @objc func configurePageViewController() {
        guard let pageVc = storyboard?.instantiateViewController(identifier: "CustomPageViewController") as? CustomPageViewController else {
            return
        }
        
        pageVc.delegate = self
        pageVc.dataSource = self
        
        addChild(pageVc)
        pageVc.didMove(toParent: self)
        
        pageVc.view.translatesAutoresizingMaskIntoConstraints = false
                        
        pageControl.layer.zPosition = 1;
        pageControl.numberOfPages = myDataSource.count
        pageControl.currentPageIndicatorTintColor = .lightGray
        pageControl.pageIndicatorTintColor = .white
        
        contentView.addSubview(pageVc.view)
        
        let views : [String : Any] = ["pageView": pageVc.view!]
        
        contentView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[pageView]-0-|",
                                                                  options: NSLayoutConstraint.FormatOptions(rawValue: 0),
                                                                  metrics: nil,
                                                                  views: views))
        
        contentView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[pageView]-0-|",
                                                                  options: NSLayoutConstraint.FormatOptions(rawValue: 0),
                                                                  metrics: nil,
                                                                  views: views))
        guard let startingVc = detailVCAt(index: currentVCIndex) else {
            return
        }
        
        pageVc.setViewControllers([startingVc], direction: .forward, animated: true)

        increaseCount()
    }
    
    func increaseCount() {
        pageControl.currentPage = currentVCIndex
        
        if currentVCIndex == 3 {
            currentVCIndex = 0
        } else {
            currentVCIndex = currentVCIndex+1
        }
    }
    
    func detailVCAt(index: Int) -> DataViewController? {
        
        if index >= myDataSource.count || myDataSource.count == 0 {
            return nil
        }
        
        guard let dataVC = storyboard?.instantiateViewController(identifier: "DataViewController") as? DataViewController else {
            return nil
        }
        
        dataVC.index = index
        dataVC.displayImgName = myDataSource[index]
        dataVC.redirectURL = urlDataSource[index]
        
        return dataVC
    }

}

extension ViewController: UIPageViewControllerDelegate, UIPageViewControllerDataSource {
    
    func presentationIndex(for _: UIPageViewController) -> Int {
        return myDataSource.count
    }
    
    func presentationCount(for _: UIPageViewController) -> Int {
        return myDataSource.count
    }
    
    func pageViewController(_: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        
        let dataVc = viewController as? DataViewController
        
        guard var currentIndex = dataVc?.index else {
            return nil
        }

        currentVCIndex = currentIndex
        
        if currentIndex == 0 {
            return nil
        }

        currentIndex -= 1
        pageControl.currentPage = currentVCIndex
    //        timerStart()
    //        increaseCount()
        return detailVCAt(index: currentIndex)
    }
    
    func pageViewController(_: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        
        let dataVc = viewController as? DataViewController
        
        guard var currentIndex = dataVc?.index else {
            return nil
        }

        if currentIndex == myDataSource.count {
            return nil
        }
        
        currentIndex += 1

        currentVCIndex = currentIndex
        pageControl.currentPage = currentVCIndex
    //        timerStart()
    //        increaseCount()
        return detailVCAt(index: currentIndex)
    }
    
    
}

My DataViewController.Swift code

import UIKit

class DataViewController: UIViewController {
    
    @IBOutlet weak var btn: UIButton!
    var displayImgName :String?
    var redirectURL :String?
    var index: Int?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        btn.setImage(UIImage.init(named: displayImgName ?? ""), for: .normal)
    }
    
    @IBAction func onClickBtn(_ sender: UIButton) {
        
        if let url = URL(string: redirectURL ?? ""), UIApplication.shared.canOpenURL(url) {
            if #available(iOS 10.0, *) {
                UIApplication.shared.open(url, options: [:], completionHandler: nil)
            } else {
                UIApplication.shared.openURL(url)
            }
        }
    }
    
}

My CustomPageViewController.Swift code

import UIKit

class CustomPageViewController: UIPageViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }
       
}

My StoryBoard image

enter image description here

2 Answers

In your code, currentIndex is getting wrong. You can use the following delegate method to get correct currentIndex.

func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool)

Also in your scheduled timer configurePageViewController methods call every 5-sec. This method creates a new instance of UIPageViewController at the time of method call and adds a child view, this will cause memory issues in the future.

I would suggest if you add UIPageViewController embedded in ContainerView using a storyboard. You can find attached Screenshot.

I have a better solution for your scenario. Please find the following code:

My ViewController.Swift code:

class ViewController: UIViewController {
    
    @IBOutlet weak var contentView: UIView!
    @IBOutlet weak var pageControl: UIPageControl!
    let myDataSource = ["Img1", "Img2", "Img3", "Img4"]
    let urlDataSource = ["https://fabric.io/", "https://about.gitlab.com/", "https://developer.apple.com/", "https://fast.com/"]
    var timer = Timer()
    var pageViewController: CustomPageViewController? {
        didSet {
            pageViewController?.objViewController = self
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        configurePageControl()
        timerStart()
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let pageViewController = segue.destination as? CustomPageViewController {
            self.pageViewController = pageViewController
        }
    }
    
    func pageViewIdexChange(_ index : Int) {
        pageControl.currentPage = index
    }
    
    func timerStart() {
        timer.invalidate()
        guard let pageViewController = pageViewController else { return }
        timer = Timer.scheduledTimer(timeInterval: 5.0, target: pageViewController, selector: #selector(pageViewController.scrollToNextViewController), userInfo: nil, repeats: true)
    }

    func configurePageControl() {
        pageControl.numberOfPages = myDataSource.count
        pageControl.currentPageIndicatorTintColor = .lightGray
        pageControl.pageIndicatorTintColor = .white
    }
    
}

My DataViewController.Swift code:

class DataViewController: UIViewController {
    
    @IBOutlet weak var btn: UIButton!
    var displayImgName :String?
    var redirectURL :String?
    var index: Int?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        btn.setImage(UIImage.init(named: displayImgName ?? ""), for: .normal)
        view.backgroundColor = .random()
    }
    
    @IBAction func onClickBtn(_ sender: UIButton) {
        
        if let url = URL(string: redirectURL ?? ""), UIApplication.shared.canOpenURL(url) {
            if #available(iOS 10.0, *) {
                UIApplication.shared.open(url, options: [:], completionHandler: nil)
            } else {
                UIApplication.shared.openURL(url)
            }
        }
    }
    
}

My CustomPageViewController.Swift code:

import UIKit

class CustomPageViewController: UIPageViewController {
    
    weak var objViewController: ViewController?
    var myDataSource: [String] = []
    var urlDataSource: [String] = []
    var currentIndex = 0
    
    private(set) lazy var orderedViewControllers: [UIViewController] = {
        // The view controllers will be shown in this order
        var vcs : [DataViewController] = []
        for i in 0 ..< myDataSource.count {
            vcs.append(detailVCAt(index: i)!)
        }
        return vcs
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        dataSource = self
        delegate = self
        
        myDataSource = objViewController?.myDataSource ?? []
        urlDataSource = objViewController?.urlDataSource ?? []
        
        if let initialViewController = orderedViewControllers.first {
            scrollToViewController(initialViewController)
        }
    }
    
    func detailVCAt(index: Int) -> DataViewController? {
        
        if index >= myDataSource.count || myDataSource.count == 0 {
            return nil
        }
        
        guard let dataVC = storyboard?.instantiateViewController(identifier: "DataViewController") as? DataViewController else {
            return nil
        }
        
        dataVC.index = index
        dataVC.displayImgName = myDataSource[index]
        dataVC.redirectURL = urlDataSource[index]
        
        return dataVC
    }
    
    //MARK: - Methods
    @objc func scrollToNextViewController() {
        if currentIndex < orderedViewControllers.count - 1 {
            scrollToViewController(currentIndex + 1)
        } else {
            scrollToViewController(0, direction: .forward)
        }
    }
    
    func scrollToViewController(_ newIndex: Int, direction: UIPageViewController.NavigationDirection? = nil) {
        var direction = direction
        if let firstViewController = viewControllers?.first,
           let currentIndex = orderedViewControllers.firstIndex(of: firstViewController) {
            if direction == nil {
                direction = newIndex >= currentIndex ? .forward : .reverse
            }
            let nextViewController = orderedViewControllers[newIndex]
            scrollToViewController(nextViewController, direction: direction!)
        }
    }
    
    func scrollToViewController(_ viewController: UIViewController,
                                direction: UIPageViewController.NavigationDirection = .forward) {
        setViewControllers([viewController],
                           direction: direction,
                           animated: true,
                           completion: { (finished) -> Void in
                            // Setting the view controller programmatically does not fire
                            // any delegate methods, so we have to manually notify the
                            // 'tutorialDelegate' of the new index.
                            self.notifyTutorialDelegateOfNewIndex()
                           })
    }
    
    private func notifyTutorialDelegateOfNewIndex() {
        
        if let firstViewController = viewControllers?.first,
           let index = orderedViewControllers.firstIndex(of: firstViewController) {
            currentIndex = index
            objViewController?.pageViewIdexChange(index)
        }
    }
    
    
}

// MARK: - UIPageViewControllerDataSource & UIPageViewControllerDelegate
extension CustomPageViewController : UIPageViewControllerDataSource , UIPageViewControllerDelegate {
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        guard let viewControllerIndex = orderedViewControllers.firstIndex(of: viewController) else {
            return nil
        }
        
        let previousIndex = viewControllerIndex - 1
        
        // User is on the first view controller and swiped left to loop to
        // the last view controller.
        guard previousIndex >= 0 else {
            return orderedViewControllers.last
        }
        
        guard orderedViewControllers.count > previousIndex else {
            return nil
        }
        
        return orderedViewControllers[previousIndex]
    }
    
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        guard let viewControllerIndex = orderedViewControllers.firstIndex(of: viewController)  else {
            return nil
        }
        
        let nextIndex = viewControllerIndex + 1
        let orderedViewControllersCount = orderedViewControllers.count
        
        // User is on the last view controller and swiped right to loop to
        // the first view controller.
        guard orderedViewControllersCount != nextIndex else {
            return orderedViewControllers.first
        }
        
        guard orderedViewControllersCount > nextIndex else {
            return nil
        }
        
        return orderedViewControllers[nextIndex]
    }
    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
        
        notifyTutorialDelegateOfNewIndex()
    }
    
}

My StoryBoard image

StoryBoard

The issue in your code is that you cannot determine the index of the currently visible view controller, inside pageViewController(_:viewControllerBefore:) and pageViewController(_:viewControllerAfter:) functions of the UIPageViewControllerDataSource, due to the fact that those delegate functions may get called more than once each time you scroll.

For example, if you are currently in index 0 and you scroll to index 1, the pageViewController(_:viewControllerAfter:) function will be called 2 times, one for index 1 and one for index 2 to preload the next page.

Having said that, you can pretty much find out the index of the currently visible view controller following this answer and using pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted:) function of the UIPageViewControllerDelegate, but with a small modification that fits your needs:

extension ViewController: UIPageViewControllerDelegate {
    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
        guard
            completed,
            let currentIndex = (viewControllers?.first as? DataViewController)?.index
        else {
            return
        }
        currentVCIndex = currentIndex
        pageControl.currentPage = currentVCIndex
    }
}

Also, there is no need to implement presentationIndex(for:) and presentationCount(for:) functions of UIPageViewControllerDataSource since you are using a custom UIPageControl. So, your extension may look like this:

extension ViewController: UIPageViewControllerDataSource {
    func pageViewController(_: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        
        let dataVc = viewController as? DataViewController
        
        guard var currentIndex = dataVc?.index else {
            return nil
        }
        
        if currentIndex == 0 {
            return nil
        }

        currentIndex -= 1
        
        return detailVCAt(index: currentIndex)
    }

    func pageViewController(_: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        
        let dataVc = viewController as? DataViewController
        
        guard var currentIndex = dataVc?.index else {
            return nil
        }

        if currentIndex == myDataSource.count {
            return nil
        }
        
        currentIndex += 1
        
        return detailVCAt(index: currentIndex)
    }
}
Related