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
}
}
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:
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
:
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.