16

I have implemented a UIPageViewController that contains two pages. On the right most page, I am able to swipe to the right, and pull the page back so that when I release, it bounces back. The same thing occurs on the left page when I swipe to the left. (The bouncing is like what happens when you reach the bottom of a safari page)

Is there a way to disable the bounce effect? Thanks!

Ivan Bartsov
  • 19,664
  • 7
  • 61
  • 59
Drew K.
  • 177
  • 1
  • 1
  • 7
  • What class is the `view` of your UIPageViewController? If it's a UIScrollView (or subclass thereof), you can set the `bounces` property to `NO` on the view. – Tim Apr 01 '14 at 20:24
  • From what I understand, UIPageViewController implements a UIScrollView, but the class itself is still UIPageViewController. – Drew K. Apr 01 '14 at 20:41
  • Not the controller itself – the controller's `view`, which is a property on UIViewController, the superclass of UIPageViewController. – Tim Apr 01 '14 at 21:21
  • The class is UIPageViewControllerContentView. – Drew K. Apr 02 '14 at 20:09

6 Answers6

18

Thus far, none of the answers actually work fully. The edge case that they all fail on is this:

  1. Scroll to page 2.
  2. Using one finger, drag towards page 1.
  3. Place a second finger on the screen and drag towards page 1.
  4. Lift the first finger.
  5. Repeat until you have dragged past page 0.

In that situation, every solution I've seen so far goes past the bounds of page 0. The core problem is that the underlying API is broken, and begins reporting a content offset relative to page 0 without calling our callback to let us know that it is showing a different page. Throughout this process, the API still claims to be showing page 1, going towards page zero even while it is really on page zero going towards page -1.

The workaround for this design flaw is remarkably ugly, but here it is:

@property (weak,nonatomic) UIPageControl *pageControl;
@property (nonatomic,assign) BOOL shouldBounce;
@property (nonatomic,assign) CGFloat lastPosition;
@property (nonatomic,assign) NSUInteger currentIndex;
@property (nonatomic,assign) NSUInteger nextIndex;

- (void)viewDidLoad {

    [super viewDidLoad];

...

    self.shouldBounce = NO;

    for (id testView in self.pageController.view.subviews) {
        UIScrollView *scrollView = (UIScrollView *)testView;
        if ([scrollView isKindOfClass:[UIScrollView class]]) {
            scrollView.delegate = self;
            // scrollView.bounces = self.shouldBounce;
        }
    }
}

- (NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController{

    return (NSInteger)self.currentIndex;
}

- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers{

    id controller = [pendingViewControllers firstObject];
    self.nextIndex = [viewControllers indexOfObject:controller];
}

- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed{

    if(completed) {
        // At this point, we can safely query the API to ensure
        // that we are fully in sync, just in case.
        self.currentIndex = [viewControllers indexOfObject:[pageViewController.viewControllers objectAtIndex:0]];
        [self.pageControl setCurrentPage:self.currentIndex];
    }

    self.nextIndex = self.currentIndex;

}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    /* The iOS page view controller API is broken.  It lies to us and tells us
       that the currently presented view hasn't changed, but under the hood, it
       starts giving the contentOffset relative to the next view.  The only
       way to detect this brain damage is to notice that the content offset is
       discontinuous, and pretend that the page changed.
     */
    if (self.nextIndex > self.currentIndex) {
        /* Scrolling forwards */

        if (scrollView.contentOffset.x < (self.lastPosition - (.9 * scrollView.bounds.size.width))) {
            self.currentIndex = self.nextIndex;
            [self.pageControl setCurrentPage:self.currentIndex];
        }
    } else {
        /* Scrolling backwards */

        if (scrollView.contentOffset.x > (self.lastPosition + (.9 * scrollView.bounds.size.width))) {
            self.currentIndex = self.nextIndex;
            [self.pageControl setCurrentPage:self.currentIndex];
        }
    }

    /* Need to calculate max/min offset for *every* page, not just the first and last. */
    CGFloat minXOffset = scrollView.bounds.size.width - (self.currentIndex * scrollView.bounds.size.width);
    CGFloat maxXOffset = (([viewControllers count] - self.currentIndex) * scrollView.bounds.size.width);

    NSLog(@"Page: %ld NextPage: %ld X: %lf MinOffset: %lf MaxOffset: %lf\n", (long)self.currentIndex, (long)self.nextIndex,
          (double)scrollView.contentOffset.x,
          (double)minXOffset, (double)maxXOffset);

    if (!self.shouldBounce) {
        CGRect scrollBounds = scrollView.bounds;
        if (scrollView.contentOffset.x <= minXOffset) {
            scrollView.contentOffset = CGPointMake(minXOffset, 0);
            // scrollBounds.origin = CGPointMake(minXOffset, 0);
        } else if (scrollView.contentOffset.x >= maxXOffset) {
            scrollView.contentOffset = CGPointMake(maxXOffset, 0);
            // scrollBounds.origin = CGPointMake(maxXOffset, 0);
        }
        [scrollView setBounds:scrollBounds];
    }
    self.lastPosition = scrollView.contentOffset.x;
}

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
    /* Need to calculate max/min offset for *every* page, not just the first and last. */
    CGFloat minXOffset = scrollView.bounds.size.width - (self.currentIndex * scrollView.bounds.size.width);
    CGFloat maxXOffset = (([viewControllers count] - self.currentIndex) * scrollView.bounds.size.width);

    if (!self.shouldBounce) {
        if (scrollView.contentOffset.x <= minXOffset) {
            *targetContentOffset = CGPointMake(minXOffset, 0);
        } else if (scrollView.contentOffset.x >= maxXOffset) {
            *targetContentOffset = CGPointMake(maxXOffset, 0);
        }
    }
}

Basically, it records the offset for each scroll event. If the scroll position has moved a distance that is impossible (I arbitrarily picked 90% of the width of the screen) in the opposite direction from the direction of scrolling, the code assumes that iOS is lying to us, and behaves as though the transition finished properly, treating the offsets as being relative to the new page instead of the old one.

SahilS
  • 396
  • 5
  • 7
dgatwood
  • 10,129
  • 1
  • 28
  • 49
  • Worked very well. Thx – Praveen May 23 '15 at 16:58
  • Can you explain how Apple got it working in their own apps? If they applied a hack similar to yours, they should realize how buggy it is. – fruitcoder Oct 18 '16 at 14:58
  • My guess is that either they don't use a page controller or they have some SPI that lets them disable it properly. – dgatwood Oct 18 '16 at 20:30
  • I've implemented this, but if afterwards you make the bug happen and then try to swipe to the next page it will bounce back to the page you're on instead of moving to the next page. – Tometoyou Jun 28 '17 at 16:33
  • You probably need to reset some state in scrollViewWillBeginDragging, but I'm not sure what. – dgatwood Jun 28 '17 at 16:43
  • Sorry I mean go through the steps you provided at the start of your answer for that edge case. I've noticed that in didFinishAnimating, it actually gives the wrong view when that edge case occurs, so it can't be relied on. That changes currentIndex to the wrong index. – Tometoyou Jun 28 '17 at 16:58
  • You might try using dispatch_after to perform the check on the next pass through the run loop, but if that doesn't work, try asking each VC for its screen coordinates. – dgatwood Jun 29 '17 at 01:44
  • Check out this solution https://stackoverflow.com/a/25167681/5653015 – iOS Lifee Oct 10 '17 at 10:46
  • Check out my solution: https://stackoverflow.com/a/55179893/4219574 – Kamil Harasimowicz Jul 18 '19 at 13:10
5

Here is a simple solution

fileprivate var currentIndex = 0
fileprivate var lastPosition: CGFloat = 0


override func viewDidLoad() {
    super.viewDidLoad()
            
    for view in view.subviews {
        if view is UIScrollView {
            (view as! UIScrollView).delegate =  self
            break
        }
    }
 }


func pageViewController(_ pageViewController: UIPageViewController,
                        didFinishAnimating finished: Bool,
                        previousViewControllers: [UIViewController],
                        transitionCompleted completed: Bool) {
    
    if completed {
        // Get current index
        let pageContentViewController = pageViewController.viewControllers![0]
        currentIndex = orderedViewControllers.index(of: pageContentViewController)!
    }
}



func scrollViewDidScroll(_ scrollView: UIScrollView) {
    self.lastPosition = scrollView.contentOffset.x
    
    if (currentIndex == orderedViewControllers.count - 1) && (lastPosition > scrollView.frame.width) {
        scrollView.contentOffset.x = scrollView.frame.width
        return
        
    } else if currentIndex == 0 && lastPosition < scrollView.frame.width {
        scrollView.contentOffset.x = scrollView.frame.width
        return
    }
}
Grigor Hakobyan
  • 774
  • 6
  • 8
0

Here is @SahilS solution implemented in Swift.

It however seems to be buggy for me.

 override func viewDidLoad() {
        super.viewDidLoad()


      for view in view.subviews {
        if view is UIScrollView {
          (view as! UIScrollView).delegate =  self

                  break
        }
      }



extension PageViewController: UIPageViewControllerDelegate, UIPageViewControllerDataSource {

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {

        guard let viewControllerIndex = orderedViewControllers?.index(of: viewController) else {
            return nil
        }

        let previousIndex = viewControllerIndex - 1

        guard previousIndex >= 0 else {
            return nil
        }

        guard (orderedViewControllers?.count)! > previousIndex else {
            return nil
        }
        print("in viewControllerBefore")
        return orderedViewControllers?[previousIndex]
    }

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {

        guard let viewControllerIndex = orderedViewControllers?.index(of: viewController) else {
            return nil
        }

        let nextIndex = viewControllerIndex + 1
        let orderedViewControllersCount = orderedViewControllers?.count

        guard orderedViewControllersCount != nextIndex else {
            return nil
        }

        print("in viewControllerAfter")
        return orderedViewControllers?[nextIndex]
    }
    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {


        if completed {
          // Get current index
          let pageContentViewController = pageViewController.viewControllers![0]

          currentIndex = (orderedViewControllers?.index(of: pageContentViewController))!
        }
      self.nextIndex = self.currentIndex

    }

    func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
        print("willTransitionTo")

      let controller = pendingViewControllers.first

      if let i = viewControllers?.index(of: controller!) {
        print("Jason is at index \(i)")
        self.currentIndex = i
      } else {
        print("Jason isn't in the array")
      }

    }


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

}





extension PageViewController: UIScrollViewDelegate {

  func scrollViewDidScroll(_ scrollView: UIScrollView) {
    /* The iOS page view controller API is broken.  It lies to us and tells us
     that the currently presented view hasn't changed, but under the hood, it
     starts giving the contentOffset relative to the next view.  The only
     way to detect this brain damage is to notice that the content offset is
     discontinuous, and pretend that the page changed.
     */

    let poop = self.lastPosition + (0.9 * scrollView.bounds.size.width)
    print("poop is \(poop)")


    if (self.nextIndex > self.currentIndex) {
      /* Scrolling forwards */

      if (scrollView.contentOffset.x < (self.lastPosition - (0.9 * scrollView.bounds.size.width))) {
        self.currentIndex = self.nextIndex;
      }
    } else {
      /* Scrolling backwards */

      if (scrollView.contentOffset.x > (self.lastPosition + (0.9 * scrollView.bounds.size.width))) {
        self.currentIndex = self.nextIndex;
      }
    }

    /* Need to calculate max/min offset for *every* page, not just the first and last. */
    let minXOffset = scrollView.bounds.size.width - (CGFloat(self.currentIndex) * scrollView.bounds.size.width);
    let maxXOffset = (CGFloat(((viewControllers?.count)! - self.currentIndex)) * scrollView.bounds.size.width)

    if (!self.shouldBounce) {
      let scrollBounds = scrollView.bounds;
      if (scrollView.contentOffset.x <= minXOffset) {
        scrollView.contentOffset = CGPoint(x: minXOffset, y: 0)
      } else if (scrollView.contentOffset.x >= maxXOffset) {
        scrollView.contentOffset = CGPoint(x: maxXOffset, y: 0)
      }
      scrollView.bounds = scrollBounds
    }
    self.lastPosition = scrollView.contentOffset.x

  }
  func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

    var scrollOffset = targetContentOffset.pointee


    let minXOffset = scrollView.bounds.size.width - (CGFloat(self.currentIndex) * scrollView.bounds.size.width);
    let maxXOffset = (CGFloat(((viewControllers?.count)! - self.currentIndex)) * scrollView.bounds.size.width)

    if (!self.shouldBounce) {
      if (scrollView.contentOffset.x <= minXOffset) {
        scrollOffset = CGPoint(x: minXOffset, y: 0)

      } else if (scrollView.contentOffset.x >= maxXOffset) {
        scrollOffset = CGPoint(x: maxXOffset, y: 0)

      }
    }


  }


}
mparrish91
  • 101
  • 3
  • 11
-1
for (UIView *view in self.pageViewController.view.subviews ) {
    if ([view isKindOfClass:[UIScrollView class]]) {
        UIScrollView *scroll = (UIScrollView *)view;
        scroll.bounces = NO;
    }
}

It's kinda hacky though -- that's probably why the original answer here got downvoted.

Community
  • 1
  • 1
  • Sorry for the late reply, but implementing this just disabled scrolling altogether. Any thoughts? – Drew K. Jun 20 '14 at 20:04
  • @user2551625 well, do you really need to use PageViewController just for 2 pages? Anyway you should disable bounce only when user wants to swipe the first and the last page. Just check the current page index in **viewControllerBeforeViewController** and **viewControllerAfterViewController** datasource methods (pageIndex = 0 and pageIndex = pages.count correspondingly) – Lisa Konysheva Jun 22 '14 at 11:54
-1

I made it.

If you want to disable the bouncing effect of UIPageViewController for the 1st page (bounce on the left) and the last page (bounce on the right), the idea is to implement the underlying scrollView's delegate:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset

To implement the delegate, you can

  1. loop the subviews of UIPageViewController.view, and find the UIScrollView to set its delegate
  2. subclass UIPageViewController

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 (NO == isPageToBounce) {
        if (_currentPage == 0 && scrollView.contentOffset.x < scrollView.bounds.size.width) {
            scrollView.contentOffset = CGPointMake(scrollView.bounds.size.width, 0);
        }
        if (_currentPage == [listVCs count]-1 && scrollView.contentOffset.x > scrollView.bounds.size.width) {
            scrollView.contentOffset = CGPointMake(scrollView.bounds.size.width, 0);
        }
    }
    // more
}

And the implementation for scrollViewWillEndDragging is to deal with a bug scenario when the user quickly swipes from left to right at the fist 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 cause, not expected).

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
    if (NO == isPageToBounce) {
        if (_currentPage == 0 && scrollView.contentOffset.x <= scrollView.bounds.size.width) {
            velocity = CGPointZero;
            *targetContentOffset = CGPointMake(scrollView.bounds.size.width, 0);
        }
        if (_currentPage == [listVCs count]-1 && scrollView.contentOffset.x >= scrollView.bounds.size.width) {
            velocity = CGPointZero;
            *targetContentOffset = CGPointMake(scrollView.bounds.size.width, 0);
        }
    }
}
Dong Ma
  • 1,073
  • 9
  • 16
-1

Also the trick, but i think it's better then dealing with pageViewController.view.subviews array

1) put your UIPageViewController on UIScrollView

2) content width have to be larger then scrollview width, for example by 10.0f

self.scrollView.contentSize = CGSizeMake(self.scrollView.frame.size.width + 10.0f, self.scrollView.frame.size.height);

3) set scroll view bounce - NO

4) set scrollview delegate, and implement

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    scrollView.contentOffset = CGPointMake(0.0, 0.0);
}
  • the way you suggested is not a solution. Scrolling up and down action sometimes is not working. – mkjwa Dec 24 '16 at 05:39