Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Disable UIPageViewController bounce

Searched a lot for this one, but couldn't find a proper solution yet.

Is it possible to disable the bounce effect of a UIPageViewController and still use the UIPageViewControllerTransitionStyleScroll?

like image 816
Mario Avatar asked Feb 15 '14 13:02

Mario


4 Answers

Disable UIPageViewController's bounce

  1. Add the <UIScrollViewDelegate> delegate to your UIPageViewController's header

  2. Set the UIPageViewController's underlying UIScrollView's delegates to their parent in viewDidLoad:

    for (UIView *view in self.view.subviews) {
        if ([view isKindOfClass:[UIScrollView class]]) {
            ((UIScrollView *)view).delegate = self;
            break;
        }
    }
    
  3. The implementation for scrollViewDidScroll is to reset the contentOffset to the origin (NOT (0,0), but (bound.size.width, 0)) when the user is reaching out of the bounds, like this:

    - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
        if (_currentPage == 0 && scrollView.contentOffset.x < scrollView.bounds.size.width) {
            scrollView.contentOffset = CGPointMake(scrollView.bounds.size.width, 0);
        } else if (_currentPage == totalViewControllersInPageController-1 && scrollView.contentOffset.x > scrollView.bounds.size.width) {
            scrollView.contentOffset = CGPointMake(scrollView.bounds.size.width, 0);
        }
    }
    
  4. Finally, the implementation for scrollViewWillEndDragging is to deal with a bug scenario when the user quickly swipes from left to right at the first page, the first page won't bounce at the left (due to the function above), but will bounce at the right caused by the (maybe) velocity of the swipe. And finally when bounced back, the UIPageViewController will trigger a page flip to the 2nd page (which is of course, not expected).

    - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
        if (_currentPage == 0 && scrollView.contentOffset.x <= scrollView.bounds.size.width) {
            *targetContentOffset = CGPointMake(scrollView.bounds.size.width, 0);
        } else if (_currentPage == totalViewControllersInPageController-1 && scrollView.contentOffset.x >= scrollView.bounds.size.width) {
            *targetContentOffset = CGPointMake(scrollView.bounds.size.width, 0);
        }
    }
    

Swift 4.0

Code to put into viewDidLoad:

for subview in self.view.subviews {
    if let scrollView = subview as? UIScrollView {
        scrollView.delegate = self
        break;
    }
}

Implementation for scrollViewDidScroll:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    if (currentPage == 0 && scrollView.contentOffset.x < scrollView.bounds.size.width) {
        scrollView.contentOffset = CGPoint(x: scrollView.bounds.size.width, y: 0);
    } else if (currentPage == totalViewControllersInPageController - 1 && scrollView.contentOffset.x > scrollView.bounds.size.width) {
        scrollView.contentOffset = CGPoint(x: scrollView.bounds.size.width, y: 0);
    }
}

Implementation for scrollViewWillEndDragging:

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    if (currentPage == 0 && scrollView.contentOffset.x <= scrollView.bounds.size.width) {
        targetContentOffset.pointee = CGPoint(x: scrollView.bounds.size.width, y: 0);
    } else if (currentPage == totalViewControllersInPageController - 1 && scrollView.contentOffset.x >= scrollView.bounds.size.width) {
        targetContentOffset.pointee = CGPoint(x: scrollView.bounds.size.width, y: 0);
    }
}
like image 53
Dong Ma Avatar answered Oct 20 '22 19:10

Dong Ma


Disable UIPageViewController's bounce

Swift 2.2

Addition to answers

1) Add UIScrollViewDelegate to UIPageViewController

extension PageViewController: UIScrollViewDelegate

2) Add to viewDidLoad

for view in self.view.subviews {
   if let scrollView = view as? UIScrollView {
      scrollView.delegate = self
   }
}

3) Add UIScrollViewDelegate methods

func scrollViewDidScroll(scrollView: UIScrollView) {
    if currentIndex == 0 && scrollView.contentOffset.x < scrollView.bounds.size.width {
        scrollView.contentOffset = CGPoint(x: scrollView.bounds.size.width, y: 0)
    } else if currentIndex == totalViewControllers - 1 && scrollView.contentOffset.x > scrollView.bounds.size.width {
        scrollView.contentOffset = CGPoint(x: scrollView.bounds.size.width, y: 0)
    }
}

func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    if currentIndex == 0 && scrollView.contentOffset.x < scrollView.bounds.size.width {
        scrollView.contentOffset = CGPoint(x: scrollView.bounds.size.width, y: 0)
    } else if currentIndex == totalViewControllers - 1 && scrollView.contentOffset.x > scrollView.bounds.size.width {
        scrollView.contentOffset = CGPoint(x: scrollView.bounds.size.width, y: 0)
    }
}
like image 21
ZAV Avatar answered Oct 20 '22 18:10

ZAV


The only working solution, 2020

Below is a complete solution that, unlike with other answers, doesn't require you to write/test your own code for tracking the index of the currently displayed page.

Assuming that your pages are stored in sourcePageViewControllers immutable array and after you've created your UIPageViewController as myPageViewController:

let scrollView = myPageViewController.view.subviews.compactMap({ $0 as? UIScrollView }).first!
scrollView.delegate = <instance of YourScrollViewDelegateClass>

And then:

extension YourScrollViewDelegateClass: UIScrollViewDelegate {

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        guard sourcePageViewControllers.count > 1 else {
            scrollView.isScrollEnabled = false
            return
        }
        
        guard let viewControllers = myPageViewController.viewControllers, viewControllers.count != 0 else { return }

        let baseRecord =
            viewControllers
            .map { [superview = myPageViewController.view!] viewController -> (viewController: UIViewController, originX: CGFloat) in
                let originX = superview.convert(viewController.view.bounds.origin, from: viewController.view).x
                return (viewController: viewController, originX: originX)
            }
            .sorted(by: { $0.originX < $1.originX })
            .first!

        guard let baseIndex = sourcePageViewControllers.firstIndex(of: baseRecord.viewController) else { return }
        let baseViewControllerOffsetXRatio = -baseRecord.originX/scrollView.bounds.width

        let progress = (CGFloat(baseIndex) + baseViewControllerOffsetXRatio)/CGFloat(sourcePageViewControllers.count - 1)
        if !(0...1 ~= progress) {
            scrollView.isScrollEnabled = false
            scrollView.isScrollEnabled = true
        }
    }

}
like image 3
Desmond Hume Avatar answered Oct 20 '22 20:10

Desmond Hume


If you keep track of your currentIndex then the below should be sufficient but its a little buggy because there is a random scenario where it stops scrolling altogether.

I think the scrollView.bounces is a little buggy, perhaps I am missing something because most of the time it works fine, if anyone is able to have a solution based on the below it would be great please.

public func scrollViewDidScroll(_ scrollView: UIScrollView) {
    scrollView.bounces = currentIndex == 0 ||
        currentIndex == controllers.count - 1
        ? false 
        : true
}
like image 3
Wael Avatar answered Oct 20 '22 18:10

Wael