iosswiftuiscrollviewuipageviewcontroller

Disable UIPageViewController Scroll


I have UIPageViewController with 3 child viewControllers. Is it possible to disable/prevent a user from scrolling to one specific viewController (i.e. User can scroll from view controller B to A, but cannot from B to C - until I later toggle permission allowing user to go from B to C).

Any guidance would be greatly appreciated!

I've been able to disable the scroll via UIPageViewController scrollView, but can't isolate disabling to a specific viewController.

func togglePageControllerScroll(shouldScroll: Bool) {
    
    for view in view.subviews {
        
        if let scrollView = view as? UIScrollView {
            
            scrollView.isScrollEnabled = shouldScroll
            break
        }
    }
}

I've also tried putting a check in UIPageViewController delegate functions viewControllerBefore and viewControllerAfter where I would return nil if the user tried scrolling from B to C, but it seems like doing so also returns nil for viewController A...

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

let previousIndex = viewControllerIndex - 1
    
guard previousIndex >= 0 else { return nil }

guard childViewControllers.count > previousIndex else { return nil }

guard let previousViewController = childViewControllers[safe: previousIndex] else { return nil }

if viewController is B && previousViewController is A {
    
    return nil
    
} else {
    
    return previousViewController
}
}

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

let nextIndex = viewControllerIndex + 1

guard childViewControllers.count != nextIndex else {
    return nil
}

guard childViewControllers.count > nextIndex else { return nil }

guard let nextViewController = childViewControllers[safe: nextIndex] else { return nil }

if viewController is B && nextViewController is C {
    
    return nil
    
} else {
    
    return nextViewController
}

}


Solution

  • One approach - which, I think, is pretty straight-forward and would be a reasonable solution...

    Add a maxPages property to your page view controller. Then, in viewControllerAfter, do this:

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        guard let viewControllerIndex = pageViewControllers.firstIndex(of: viewController) else { return nil }
        
        let nextIndex = viewControllerIndex + 1
        
        // if we are at the Last controller,
        //  OR
        // we are at the "maxPages" controller
        //  return nil
        guard nextIndex < pageViewControllers.count, nextIndex < maxPages else { return nil }
        
        return pageViewControllers[nextIndex]
    }
    

    Start maxPages at 2, then in the class that is controlling your page view controller, when you want to allow the user to go to the 3rd page, change the maxPages property to 3.

    Here is a quick example...

    We'll start with a simple "page" view controller - a label centered vertically:

    class ExamplePageVC: UIViewController {
        
        let theLabel: UILabel = {
            let v = UILabel()
            v.translatesAutoresizingMaskIntoConstraints = false
            v.backgroundColor = .white
            v.textAlignment = .center
            return v
        }()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.addSubview(theLabel)
            NSLayoutConstraint.activate([
                theLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                theLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
                theLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.9),
            ])
            
        }
        
    }
    

    Then, a view controller to hold our page view controller, along with a switch to Allow/Not-Allow scrolling to the 3rd page:

    class PagesViewController: UIViewController {
    
        var myPVC: MyPageViewController!
        
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // a label and toggle switch to enable/disable the 3rd page
            let label = UILabel()
            label.text = "Allow 3rd Page?"
            
            let sw = UISwitch()
            sw.isOn = false
            sw.addTarget(self, action: #selector(switchChanged(_:)), for: .valueChanged)
            
            let stack = UIStackView(arrangedSubviews: [label, sw])
            stack.axis = .horizontal
            stack.spacing = 8
            
            // a UIView to hold the page view controller
            let pvcContainer = UIView()
            pvcContainer.backgroundColor = .gray
    
            [stack, pvcContainer].forEach { v in
                v.translatesAutoresizingMaskIntoConstraints = false
                view.addSubview(v)
            }
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                
                pvcContainer.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                pvcContainer.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                pvcContainer.centerYAnchor.constraint(equalTo: g.centerYAnchor),
                pvcContainer.heightAnchor.constraint(equalTo: pvcContainer.widthAnchor, multiplier: 0.75),
    
                stack.bottomAnchor.constraint(equalTo: pvcContainer.topAnchor, constant: -20.0),
                stack.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                
            ])
    
            let pvc = MyPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
            addChild(pvc)
            pvc.view.translatesAutoresizingMaskIntoConstraints = false
            pvcContainer.addSubview(pvc.view)
            
            NSLayoutConstraint.activate([
                
                pvc.view.topAnchor.constraint(equalTo: pvcContainer.topAnchor),
                pvc.view.leadingAnchor.constraint(equalTo: pvcContainer.leadingAnchor),
                pvc.view.trailingAnchor.constraint(equalTo: pvcContainer.trailingAnchor),
                pvc.view.bottomAnchor.constraint(equalTo: pvcContainer.bottomAnchor),
                
            ])
    
            pvc.didMove(toParent: self)
            
            myPVC = pvc
        }
        
        @objc func switchChanged(_ sender: UISwitch) {
            let n: Int = sender.isOn ? 3 : 2
            myPVC.maxPages = n
        }
    
    }
    

    and our custom Page View Controller:

    class MyPageViewController: UIPageViewController {
        
        var maxPages: Int = 2 {
            didSet {
                // get the current page index
                var n = currentIndex
                // set controllers, keeping the current page in view
                //  unless, we are decreasing the count and a higher-index page is showing
                if n > maxPages - 1 {
                    n = maxPages - 1
                }
                setViewControllers([pageViewControllers[n]], direction: .forward, animated: false, completion: nil)
            }
        }
    
        // so we can get the index of the current page
        var currentIndex: Int {
            guard let vc = viewControllers?.first else { return 0 }
            return pageViewControllers.firstIndex(of: vc) ?? 0
        }
        
        let colors: [UIColor] = [
            .systemRed,
            .systemGreen,
            .systemBlue,
        ]
        
        var pageViewControllers: [UIViewController] = [UIViewController]()
        
        override init(transitionStyle style: UIPageViewController.TransitionStyle, navigationOrientation: UIPageViewController.NavigationOrientation, options: [UIPageViewController.OptionsKey : Any]? = nil) {
            super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
        }
        
        required init?(coder: NSCoder) {
            super.init(coder: coder)
        }
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            dataSource = self
            
            // instantiate all "pages"
            
            for i in 0..<colors.count {
                let vc = ExamplePageVC()
                vc.theLabel.text = "Page: \(i)"
                vc.view.backgroundColor = colors[i]
                pageViewControllers.append(vc)
            }
            
            setViewControllers([pageViewControllers[0]], direction: .forward, animated: false, completion: nil)
        }
        
    }
    
    extension MyPageViewController: UIPageViewControllerDataSource {
        func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
            
            guard let viewControllerIndex = pageViewControllers.firstIndex(of: viewController) else { return nil }
            
            let previousIndex = viewControllerIndex - 1
            
            guard previousIndex >= 0 else { return nil }
            
            return pageViewControllers[previousIndex]
        }
        
        func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
            guard let viewControllerIndex = pageViewControllers.firstIndex(of: viewController) else { return nil }
            
            let nextIndex = viewControllerIndex + 1
            
            // if we are at the Last controller,
            //  OR
            // we are at the "maxPages" controller
            //  return nil
            guard nextIndex < pageViewControllers.count, nextIndex < maxPages else { return nil }
            
            return pageViewControllers[nextIndex]
        }
        
    }
    

    It will look like this:

    enter image description here enter image description here

    Arrays are zero-based, so we've set the labels to match the array index.

    When the switch is Off, we can only scroll back-and-forth between Page 0 and Page 1.

    When the switch is On, we can scroll on to the 3rd page - Page 2:

    enter image description here

    Notice that we have a little bit of code inside the didSet {} block of our maxPages var. When we set maxPages to a new value, we need to "re-set" the view controllers array of our Page View Controller, while keeping the current page visible.