skip to Main Content

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


  1. 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

    Login or Signup to reply.
  2. 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)
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search