148

In my case parent UIViewController contains UIPageViewController which contains UINavigationController which contains UIViewController. I need to add a swipe gesture to the last view controller, but swipes are handled as if they belong to page view controller. I tried to do this both programmatically and via xib but with no result.

So as I understand I can't achieve my goal until UIPageViewController handles its gestures. How to solve this issue?

Bartłomiej Semańczyk
  • 59,234
  • 49
  • 233
  • 358
user2159978
  • 2,629
  • 2
  • 16
  • 14
  • 9
    Use `pageViewControllerObject.view.userInteractionEnabled = NO;` – Srikanth Feb 28 '14 at 19:00
  • 9
    incorrect, because it blocks all its content too. But I need to block its gestures only – user2159978 Mar 03 '14 at 06:45
  • Why are you doing this? What behaviour do you want from the app? It sounds like you're overcomplicating the architecture. Can you explain what behaviour you are aiming for. – Fogmeister Mar 03 '14 at 09:59
  • 1
    One of pages in `UIPageViewController` has `UINavigationController`. The customer wants to pop navigation controller on swipe gesture (if it is possible), but page view controller handle swipes instead – user2159978 Mar 03 '14 at 10:07
  • 1
    @Srikanth thanks, that's exactly what I needed! My page view was crashing when I triggered a page change programatically and someone interrupted it with swiping, so I had to disable swiping temporarily every time the page change is triggered in code... – Kuba Suder Jun 18 '15 at 17:12
  • Event I want to disable that open gesture on swipe. I just want to open side bar control on button click. – Kushal Vora Jan 09 '17 at 14:00
  • try to use this way, it works > https://stackoverflow.com/a/38711162/6183248 – Deepak Singh Jun 16 '20 at 13:02

25 Answers25

293

The documented way to prevent the UIPageViewController from scrolling is to not assign the dataSource property. If you assign the data source it will move into 'gesture-based' navigation mode which is what you're trying to prevent.

Without a data source you manually provide view controllers when you want to with setViewControllers:direction:animated:completion method and it will move between view controllers on demand.

The above can be deduced from Apple's documentation of UIPageViewController (Overview, second paragraph):

To support gesture-based navigation, you must provide your view controllers using a data source object.

Jessedc
  • 12,320
  • 3
  • 50
  • 63
  • 1
    I prefer this solution as well. You can save off the old datasource in a variable, set the datasource to nil, and then restore the old datasource to re-enable scrolling. Not all that clean, but much better than traversing the underlying view hierarchy to find the scroll view. – Matt R Aug 13 '14 at 20:50
  • 3
    Disabling the data source causes problems when doing so in response to keyboard notifications for text field subviews. Iterating over the view's subviews to find the UIScrollView appears to be more reliable. – A. R. Younce Dec 01 '14 at 18:13
  • 10
    Doesn't this remove the dots at the bottom of the screen? And if you have no swiping and no dots, there's not much point in using a PageViewController at all. I'm guessing the asker wants to retain the dots. – Leo Flaherty Mar 05 '15 at 10:27
  • This helped to solve an issue I was facing when the pages are loaded asynchronously and the user makes a swipe gesture. Bravo :) – Ja͢ck Mar 11 '15 at 06:47
  • 1
    A. R Younce is correct. If you are disabling the datasource of the PageViewController in response to a keyboard notification (ie: you don't want user to scroll horizontally when the keyboard is up) then your keyboard will immediately disappear when you nil out the PageViewController's data source. – micnguyen Mar 23 '15 at 03:48
  • i cannot thank you enough for this... i was going in circles and circles to solve a bug i was having in my code where different gestures were conflicting each other. this have saved me alot of time – Abolfoooud Jul 10 '15 at 11:50
  • Unfortunately, this also hides the page control. – Marcus Adams Oct 30 '15 at 12:36
  • It does not hide the page control if you have your viewControllers array set. – Alexandre G. Feb 27 '18 at 15:30
  • try this it works https://stackoverflow.com/a/38711162/6183248 – Deepak Singh Jun 16 '20 at 13:03
83
for (UIScrollView *view in self.pageViewController.view.subviews) {

    if ([view isKindOfClass:[UIScrollView class]]) {

        view.scrollEnabled = NO;
    }
}
user2159978
  • 2,629
  • 2
  • 16
  • 14
  • 6
    This one keeps the page control, which is nice. – Marcus Adams Oct 30 '15 at 12:37
  • 1
    This is a great approach since it keeps the page control (small dots). You will want to update them when making the page change manually. I used this answer for this http://stackoverflow.com/a/28356383/1071899 – Simon Bøgh Jul 14 '16 at 21:06
  • 1
    Why not `for (UIView *view in self.pageViewController.view.subviews) {` – dengApro Apr 04 '18 at 01:38
  • Please note that users will still be able to navigate between pages by tapping the page control in the left and right half. – MasterCarl May 08 '19 at 13:09
70

I translate answer of user2159978 to Swift 5.1

func removeSwipeGesture(){
    for view in self.pageViewController!.view.subviews {
        if let subView = view as? UIScrollView {
            subView.isScrollEnabled = false
        }
    }
}
Viktor Malyi
  • 2,298
  • 2
  • 23
  • 40
lee
  • 7,955
  • 8
  • 44
  • 60
37

Implementing @lee's (@user2159978's) solution as an extension:

extension UIPageViewController {
    var isPagingEnabled: Bool {
        get {
            var isEnabled: Bool = true
            for view in view.subviews {
                if let subView = view as? UIScrollView {
                    isEnabled = subView.isScrollEnabled
                }
            }
            return isEnabled
        }
        set {
            for view in view.subviews {
                if let subView = view as? UIScrollView {
                    subView.isScrollEnabled = newValue
                }
            }
        }
    }
}

Usage: (in UIPageViewController)

self.isPagingEnabled = false
LinusGeffarth
  • 27,197
  • 29
  • 120
  • 174
8

Edit: this answer works for page curl style only. Jessedc's answer is far better: works regardless of the style and relies on documented behavior.

UIPageViewController exposes its array of gesture recognizers, which you could use to disable them:

// myPageViewController is your UIPageViewController instance
for (UIGestureRecognizer *recognizer in myPageViewController.gestureRecognizers) {
    recognizer.enabled = NO;
}
Community
  • 1
  • 1
Austin
  • 5,625
  • 1
  • 29
  • 43
8

I've been fighting this for a while now and thought I should post my solution, following on from Jessedc's answer; removing the PageViewController's datasource.

I added this to my PgeViewController class (linked to my page view controller in the storyboard, inherits both UIPageViewController and UIPageViewControllerDataSource):

static func enable(enable: Bool){
    let appDelegate  = UIApplication.sharedApplication().delegate as! AppDelegate
    let pageViewController = appDelegate.window!.rootViewController as! PgeViewController
    if (enable){
        pageViewController.dataSource = pageViewController
    }else{
        pageViewController.dataSource = nil
    }
}

This can then be called when each sub view appears (in this case to disable it);

override func viewDidAppear(animated: Bool) {
    PgeViewController.enable(false)
}

I hope this helps someone out, its not as clean as I would like it but doesn't feel too hacky etc.

EDIT: If someone wants to translate this into Objective-C please do :)

Jamie Robinson
  • 832
  • 10
  • 16
6

A useful extension of UIPageViewController to enable and disable swipe.

extension UIPageViewController {

    func enableSwipeGesture() {
        for view in self.view.subviews {
            if let subView = view as? UIScrollView {
                subView.isScrollEnabled = true
            }
        }
    }

    func disableSwipeGesture() {
        for view in self.view.subviews {
            if let subView = view as? UIScrollView {
                subView.isScrollEnabled = false
            }
        }
    }
}
jbustamante
  • 77
  • 1
  • 2
5

If you want your UIPageViewController to maintain it's ability to swipe, while allowing your content controls to use their features (Swipe to delete, etc), just turn off canCancelContentTouches in the UIPageViewController.

Put this in your UIPageViewController's viewDidLoad func. (Swift)

if let myView = view?.subviews.first as? UIScrollView {
    myView.canCancelContentTouches = false
}

The UIPageViewController has an auto-generated subview that handles the gestures. We can prevent these subviews from cancelling content gestures.

From...

Swipe to delete on a tableView that is inside a pageViewController

Carter Medlin
  • 11,857
  • 5
  • 62
  • 68
  • Thanks Carter. I've tried your solution. However, there are still times pageViewController over-take the horizontal swipe action away from the child table view. How can I ensure that when user swipes on the child table view, the table view is always prioritized to get the gesture first? – David Liu Feb 18 '19 at 10:19
  • The best answer to this question! – Yev Kanivets Jun 14 '21 at 07:23
5

Swifty way for @lee answer

extension UIPageViewController {
    var isPagingEnabled: Bool {
        get {
            return scrollView?.isScrollEnabled ?? false
        }
        set {
            scrollView?.isScrollEnabled = newValue
        }
    }

    var scrollView: UIScrollView? {
        return view.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView
    }
}
Szymon W
  • 489
  • 8
  • 15
4

Here is my solution in swift

extension UIPageViewController {
    var isScrollEnabled: Bool {
        set {
            (self.view.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView)?.isScrollEnabled = newValue
        }
        get {
            return (self.view.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView)?.isScrollEnabled ?? true
        }
    }
}
CallMeMeow
  • 49
  • 1
3

I solved it like this (Swift 4.1)

if let scrollView = self.view.subviews.filter({$0.isKind(of: UIScrollView.self)}).first as? UIScrollView {
             scrollView.isScrollEnabled = false
}
Darko
  • 615
  • 7
  • 21
3

pageViewController.view.isUserInteractionEnabled = false

This will disable all interaction with the pages. If you need to user to be able to interact with the content - this is not the solution for you.

gutte
  • 1,373
  • 2
  • 19
  • 31
1

There's a much simpler approach than most answers here suggest, which is to return nil in the viewControllerBefore and viewControllerAfter dataSource callbacks.

This disables the scrolling gesture on iOS 11+ devices, while keeping the possibility to use the dataSource (for things such as the presentationIndex / presentationCount used for the page indicator)

It also disables navigation via. the pageControl (the dots in the bottom) for iOS 11-13. On iOS 14, the bottom dots navigation can be disabled using a UIAppearance proxy.

extension MyPageViewController: UIPageViewControllerDataSource {
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        return nil
    }
    
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        return nil
    }
}
Claus Jørgensen
  • 25,882
  • 9
  • 87
  • 150
1

More efficient way with a return, call this method on viewdidload (Swift 5):

private func removeSwipeGesture() {
    self.pageViewController?.view.subviews.forEach({ view in
        if let subView = view as? UIScrollView {
            subView.isScrollEnabled = false
            return
        }
    })
}
Medhi
  • 2,656
  • 23
  • 16
0

Similar to @user3568340 answer

Swift 4

private var _enabled = true
    public var enabled:Bool {
        set {
            if _enabled != newValue {
                _enabled = newValue
                if _enabled {
                    dataSource = self
                }
                else{
                    dataSource = nil
                }
            }
        }
        get {
            return _enabled
        }
    }
0

Translating @user2159978's response to C#:

foreach (var view in pageViewController.View.Subviews){
   var subView = view as UIScrollView;
   if (subView != null){
     subView.ScrollEnabled = enabled;
   }
}
neurona.dev
  • 1,347
  • 1
  • 14
  • 12
0

Thanks to @user2159978's answer.

I make it a little more understandable.

- (void)disableScroll{
    for (UIView *view in self.pageViewController.view.subviews) {
        if ([view isKindOfClass:[UIScrollView class]]) {
            UIScrollView * aView = (UIScrollView *)view;
            aView.scrollEnabled = NO;
        }
    }
}
dengApro
  • 3,848
  • 2
  • 27
  • 41
0

(Swift 4) You can remove gestureRecognizers of your pageViewController:

pageViewController.view.gestureRecognizers?.forEach({ (gesture) in
            pageViewController.view.removeGestureRecognizer(gesture)
        })

If you prefer in extension:

extension UIViewController{
    func removeGestureRecognizers(){
        view.gestureRecognizers?.forEach({ (gesture) in
            view.removeGestureRecognizer(gesture)
        })
    }
}

and pageViewController.removeGestureRecognizers

harman virk
  • 7
  • 1
  • 3
Miguel
  • 957
  • 10
  • 12
0

Declare it like this:

private var scrollView: UIScrollView? {
    return pageViewController.view.subviews.compactMap { $0 as? UIScrollView }.first
}

Then use it like this:

scrollView?.isScrollEnabled = true //false
Bartłomiej Semańczyk
  • 59,234
  • 49
  • 233
  • 358
0

The answers I found look very confusing or incomplete to me so here is a complete and configurable solution:

Step 1:

Give each of your PVC elements the responsibility to tell whether left and right scrolling are enabled or not.

protocol PageViewControllerElement: class {
    var isLeftScrollEnabled: Bool { get }
    var isRightScrollEnabled: Bool { get }
}
extension PageViewControllerElement {
    // scroll is enabled in both directions by default
    var isLeftScrollEnabled: Bool {
        get {
            return true
        }
    }

    var isRightScrollEnabled: Bool {
        get {
            return true
        }
    }
}

Each of your PVC view controllers should implement the above protocol.

Step 2:

In your PVC controllers, disable the scroll if needed:

extension SomeViewController: PageViewControllerElement {
    var isRightScrollEnabled: Bool {
        get {
            return false
        }
    }
}

class SomeViewController: UIViewController {
    // ...    
}

Step 3:

Add the effective scroll lock methods to your PVC:

class PVC: UIPageViewController, UIPageViewDelegate {
    private var isLeftScrollEnabled = true
    private var isRightScrollEnabled = true
    // ...

    override func viewDidLoad() {
        super.viewDidLoad()
        // ...
        self.delegate = self
        self.scrollView?.delegate = self
    }
} 

extension PVC: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        print("contentOffset = \(scrollView.contentOffset.x)")

        if !self.isLeftScrollEnabled {
            disableLeftScroll(scrollView)
        }
        if !self.isRightScrollEnabled {
            disableRightScroll(scrollView)
        }
    }

    private func disableLeftScroll(_ scrollView: UIScrollView) {
        let screenWidth = UIScreen.main.bounds.width
        if scrollView.contentOffset.x < screenWidth {
            scrollView.setContentOffset(CGPoint(x: screenWidth, y: 0), animated: false)
        }
    }

    private func disableRightScroll(_ scrollView: UIScrollView) {
        let screenWidth = UIScreen.main.bounds.width
        if scrollView.contentOffset.x > screenWidth {
            scrollView.setContentOffset(CGPoint(x: screenWidth, y: 0), animated: false)
        }
    }
}

extension UIPageViewController {
    var scrollView: UIScrollView? {
        return view.subviews.filter { $0 is UIScrollView }.first as? UIScrollView
    }
}

Step 4:

Update scroll related attributes when reaching a new screen (if you transition to some screen manually don't forget to call the enableScroll method):

func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
    let pageContentViewController = pageViewController.viewControllers![0]
    // ...

    self.enableScroll(for: pageContentViewController)
}

private func enableScroll(for viewController: UIViewController) {
    guard let viewController = viewController as? PageViewControllerElement else {
        self.isLeftScrollEnabled = true
        self.isRightScrollEnabled = true
        return
    }

    self.isLeftScrollEnabled = viewController.isLeftScrollEnabled
    self.isRightScrollEnabled = viewController.isRightScrollEnabled

    if !self.isLeftScrollEnabled {
        print("Left Scroll Disabled")
    }
    if !self.isRightScrollEnabled {
        print("Right Scroll Disabled")
    }
}
michael-martinez
  • 767
  • 6
  • 24
0

You can implement the UIPageViewControllerDataSource protocol and return nil for the previousViewController and nextViewController methods. This will prevent the UIPageViewController from being able to swipe to the next or previous page.

    fileprivate func canSwipeToNextViewController() -> Bool {
        guard
            currentIndex < controllers.count,
            let controller = controllers[currentIndex] as? OnboardingBaseViewController,
            controller.canSwipeToNextScreen
        else {
            return false
        }

        return true
    }
}

// MARK: - UIPageViewControllerDataSource

extension ViewController: UIPageViewControllerDataSource {
    func presentationCount(for pageViewController: UIPageViewController) -> Int {
        controllers.count
    }

    func presentationIndex(for pageViewController: UIPageViewController) -> Int {
        currentIndex
    }

    func pageViewController(
        _ pageViewController: UIPageViewController,
        viewControllerBefore viewController: UIViewController
    ) -> UIViewController? {
        if let index = controllers.firstIndex(of: viewController) {
            if index > 0 {
                currentIndex -= 1
                return controllers[index - 1]
            } else {
                // Return nil to prevent swiping to the previous page
                return nil
            }
        }

        return nil
    }

    func pageViewController(
        _ pageViewController: UIPageViewController,
        viewControllerAfter viewController: UIViewController
    ) -> UIViewController? {
        if let index = controllers.firstIndex(of: viewController) {
            if index < controllers.count - 1,
               canSwipeToNextViewController() {
                currentIndex += 1
                return controllers[index + 1]
            } else {
                // Return nil to prevent swiping to the next page
                return nil
            }
        }

        return nil
    }
}

Remember to set the dataSource property of the UIPageViewController to the view controller that implements the UIPageViewControllerDataSource protocol.

I hope that helps.

Shahriyar
  • 520
  • 7
  • 18
0
func removeSwipeGesture(){
    for view in self.pageViewController!.view.subviews {
        if let subView = view as? UIScrollView {
            subView.isScrollEnabled = false
        }
    }

}

Assuming you have created pageControl seperately and linked with pagecontroller appropriately.

private let pageControl = PageControl()
pageControl.delegate = self
pageControl.numberOfPages = pages.count
pageControl.addTarget ....

if you want to hide the single dot which no longer needed since you're datasource doesn't have another viewControllers you can disable like this.

pageControl.hidesForSinglePage = true
Anees
  • 514
  • 5
  • 20
-1

Enumerating the subviews to find the scrollView of a UIPageViewController didn't work for me, as I can't find any scrollView in my page controller subclass. So what I thought of doing is to disable the gesture recognizers, but careful enough to not disable the necessary ones.

So I came up with this:

if let panGesture = self.gestureRecognizers.filter({$0.isKind(of: UIPanGestureRecognizer.self)}).first           
    panGesture.isEnabled = false        
}

Put that inside the viewDidLoad() and you're all set!

Glenn Posadas
  • 12,555
  • 6
  • 54
  • 95
-1
 override func viewDidLayoutSubviews() {
    for View in self.view.subviews{
        if View.isKind(of: UIScrollView.self){
            let ScrollV = View as! UIScrollView
            ScrollV.isScrollEnabled = false
        }

    }
}

Add this in your pageviewcontroller class. 100% working

Pathak Ayush
  • 726
  • 12
  • 24
-1

just add this control property at your UIPageViewController subclass:

var isScrollEnabled = true {
    didSet {
        for case let scrollView as UIScrollView in view.subviews {
            scrollView.isScrollEnabled = isScrollEnabled
        }
    }
}
Stanislau Baranouski
  • 1,425
  • 1
  • 18
  • 22