58

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?

Mario
  • 2,431
  • 6
  • 27
  • 34

17 Answers17

84

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);
    }
}
Aleksandr Honcharov
  • 2,343
  • 17
  • 30
Dong Ma
  • 1,073
  • 9
  • 16
  • I don't get this line does *targetContentOffset = CGPointMake(scrollView.bounds.size.width, 0); Can you pelase explain? – Andrespch Dec 15 '14 at 01:04
  • @Andrespch the underlying scrollview of UIPageViewController will place the **visible** ViewController's view at offset: (scrollView.bounds.size.width, 0). i.e. The previous ViewController's view is placed at location (0,0)~(scrollView.bounds.size.width,0), and next ViewController's view is placed at location (scrollView.bounds.size.width*2, 0)~(scrollView.bounds.size.width*3,0) when the user scrolls the UIPageViewController to reveal the previous and next views – Dong Ma Dec 15 '14 at 06:12
  • 4
    @DongMa What type are `isPageToBounce` and `_currentPage` supposed to be? I'm having trouble understanding the code. Can you explain step-by-step please? I think this is the only solution to this problem on the internet (have been looking for hours). – apolo Feb 13 '15 at 19:26
  • @espitia `isPageToBounce` is a BOOL, determine if the page view controller is supposed to bounce or not, similar to UIScrollView.bounce. `_currentPage` is the index of the view controllers, i.e. `listVCs`, displayed by the page view controller, the most left view controller has the index of 0. You can also compare the view controller's pointer instead of comparing the index. The algorithm takes control of the underlying scroll view of the page view controller, and checks the offset to see if the page view is bouncing or not. If yes, it resets the scroll view's offset to suppress bouncing. – Dong Ma Feb 14 '15 at 03:03
  • @DongMa why do `velocity = CGPointZero`? Since `velocity` is a value type, it has no effect. – Adlai Holler Apr 17 '15 at 19:35
  • @AdlaiHoller Yes, you are right. I misunderstood this parameter as an input-output argument (I had thought that my code was: *velocity = CGPointZero...). This line of code shall be removed. – Dong Ma Apr 19 '15 at 05:50
  • 1
    Thank you ;) I was trying to figure this one out for far too long :) – pasql Apr 22 '15 at 16:55
  • @DongMa How do I make that page swiping calls these two delegated. I tried in both VC i) where the PageVC is embedded and second in page view controller. Actually issue is my entire page is in scrollview where around 200 point height is occupied by pageVC. So I really have no idea what do i do, any suggestion would be of great help. – zeal Jul 29 '15 at 16:33
  • @zeal Are you talking about setting the underlying scrollview's delegates? I've pointed it out in my answer "To implement the delegate, you can:", and have given 2 solutions. The details can be found in other questions, like this: http://stackoverflow.com/a/29023923/1016214 – Dong Ma Jul 31 '15 at 08:30
  • Try the solution in http://stackoverflow.com/questions/22795883/disable-bounce-effect-in-uipageviewcontroller – dgatwood Aug 19 '16 at 21:28
  • 10
    Solution is incomplete, please add some more explanation on how to track the current page. – Benzy Jan 06 '17 at 15:17
  • The problem with this code is that it prevents the bounces on the "external" View Controllers but not on the view controllers in between –  Jun 02 '20 at 19:32
  • can anyone share the same logic for Arabic(RTL)? – iOS Lifee Dec 02 '20 at 14:21
18

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)
    }
}
Community
  • 1
  • 1
ZAV
  • 371
  • 4
  • 12
5

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

}
Desmond Hume
  • 8,037
  • 14
  • 65
  • 112
4

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
}
Wael
  • 489
  • 6
  • 19
  • What a nice and simple solution! More easily: scrollView.bounces = !(currentIndex == .zero || currentIndex == controllers.count - 1) – jason d Dec 09 '22 at 19:47
1

I wasn't sure how to correctly manage the currentIndex but ended up doing

extension Main: UIPageViewControllerDelegate {
    func pageViewController(pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
        if completed {
            guard let viewController = pageViewController.viewControllers?.first,
                index = viewControllerDatasource.indexOf(viewController) else {
                fatalError("Can't prevent bounce if there's not an index")
            }
            currentIndex = index
        }
    }
}
Ashley Mills
  • 50,474
  • 16
  • 129
  • 160
mwright
  • 4,099
  • 7
  • 30
  • 40
1

Another option is to set ScrollView.bounce = false. It solved my problem with pageViewController's(Of course not about ScrollView) scrolling bounce. Bounce is disabled, and all page can scroll without bounces.

colde
  • 3,192
  • 1
  • 15
  • 26
PERIPERI
  • 73
  • 9
  • 1
    This is wrong advice—UIPageViewController stops horizontally paging if its scrollview disables bounce. – thefaj Jun 02 '21 at 20:34
1

@Dong Ma's approach is perfect but it can be a little bit improved and simplified.

Code to put into viewDidLoad:

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

Implementation for scrollViewDidScroll:

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

Implementation for scrollViewWillEndDragging:

public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    if (currentPage == 0 && scrollView.contentOffset.x <= scrollView.bounds.size.width) || (currentPage == totalNumberOfPages - 1 && scrollView.contentOffset.x >= scrollView.bounds.size.width) {
      targetContentOffset.pointee = CGPoint(x: scrollView.bounds.size.width, y: 0)
    }
  }
anonymous
  • 1,320
  • 5
  • 21
  • 37
1

Edited answer of Dong Ma, where:

  • added - respects layout direction (Hebrew for example)
  • fixed - wrong counting currentIndex when swipes very quick

Info:

  • Written in Swift 5.0
  • Builded and tested in Xcode 10.2.1
  • iOS 12.0

How to:

  1. Let's assume we have a UIViewController where UIPageViewController is added as child VC.
class ViewController: UIViewController {
    var pageNavigationController: UIPageViewController! 

    private var lastPosition: CGFloat
    private var nextIndex: Int
    var currentIndex: Int     

    // rest of UI's setups  
}
  1. Set ViewController as delegate of UIPageViewController:
extension ViewController: UIPageViewControllerDataSource {

    func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
        guard
            let currentVisibleViewController = pageViewController.viewControllers?.first,
            let nextIndex = pageViewControllers.firstIndex(of: currentVisibleViewController)
        else {
            return
        }

        self.nextIndex = nextIndex
    }

    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
        if completed, let currentVisibleViewController = pageViewController.viewControllers?.first, let newIndex = pageViewControllers.firstIndex(of: currentVisibleViewController) {
            self.currentIndex = newIndex
        }

        self.nextIndex = self.currentIndex
    }
}
  1. Set ViewController as datasource of UIPageController:
extension ViewController: UIPageViewControllerDataSource {

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        // provide next VC
    }

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        // provide prev VC
    }

    // IMPORTANT: that's the key why it works, don't forget to add it
    func presentationIndex(for pageViewController: UIPageViewController) -> Int {
        return currentIndex
    }
}
  1. "Disable" bouncing by setting ViewController as delegate of UIPageViewController's UIScrollView:
// MARK: - UIScrollViewDelegate (disable bouncing for UIPageViewController)
extension BasePaginationVC: UIScrollViewDelegate {

    func attachScrollViewDelegate() {
        for subview in pageNavigationController.view.subviews {
            if let scrollView = subview as? UIScrollView {
                scrollView.delegate = self
                lastPosition = scrollView.contentOffset.x
                break
            }
        }
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        switch UIView.userInterfaceLayoutDirection(for: view.semanticContentAttribute) {
        case .leftToRight:
            if nextIndex > currentIndex {
                if scrollView.contentOffset.x < (lastPosition - (0.9 * scrollView.bounds.size.width)) {
                    currentIndex = nextIndex
                }
            } else {
                if scrollView.contentOffset.x > (lastPosition + (0.9 * scrollView.bounds.size.width)) {
                    currentIndex = nextIndex
                }
            }

            if currentIndex == 0 && scrollView.contentOffset.x < scrollView.bounds.size.width {
                scrollView.contentOffset = CGPoint(x: scrollView.bounds.size.width, y: 0)
            } else if currentIndex == pageViewControllers.count - 1 && scrollView.contentOffset.x > scrollView.bounds.size.width {
                scrollView.contentOffset = CGPoint(x: scrollView.bounds.size.width, y: 0)
            }
        case .rightToLeft:
            if nextIndex > currentIndex {
                if scrollView.contentOffset.x > (lastPosition + (0.9 * scrollView.bounds.size.width)) {
                    currentIndex = nextIndex
                }
            } else {
                if scrollView.contentOffset.x < (lastPosition - (0.9 * scrollView.bounds.size.width)) {
                    currentIndex = nextIndex
                }
            }

            if currentIndex == pageViewControllers.count - 1 && scrollView.contentOffset.x < scrollView.bounds.size.width {
                scrollView.contentOffset = CGPoint(x: scrollView.bounds.size.width, y: 0)
            } else if currentIndex == 0 && scrollView.contentOffset.x > scrollView.bounds.size.width {
                scrollView.contentOffset = CGPoint(x: scrollView.bounds.size.width, y: 0)
            }
        @unknown default:
            fatalError("unknown default")
        }

        lastPosition = scrollView.contentOffset.x
    }

    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        switch UIView.userInterfaceLayoutDirection(for: view.semanticContentAttribute) {
        case .leftToRight:
            if currentIndex == 0 && scrollView.contentOffset.x <= scrollView.bounds.size.width {
                targetContentOffset.pointee = CGPoint(x: scrollView.bounds.size.width, y: 0)
            } else if currentIndex == pageViewControllers.count - 1 && scrollView.contentOffset.x >= scrollView.bounds.size.width {
                targetContentOffset.pointee = CGPoint(x: scrollView.bounds.size.width, y: 0)
            }
        case .rightToLeft:
            if currentIndex == pageViewControllers.count - 1 && scrollView.contentOffset.x <= scrollView.bounds.size.width {
                targetContentOffset.pointee = CGPoint(x: scrollView.bounds.size.width, y: 0)
            } else if currentIndex == 0 && scrollView.contentOffset.x >= scrollView.bounds.size.width {
                targetContentOffset.pointee = CGPoint(x: scrollView.bounds.size.width, y: 0)
            }
        @unknown default:
            fatalError("unknown default")
        }
    }
}
Kamil Harasimowicz
  • 4,684
  • 5
  • 32
  • 58
1

My solution in Swift 5
In my scenario, I first load the UIPageViewController on the second page. And I have a total of three pages so I open on the middle one.

Here's the code of my UIPageViewController

import UIKit

class MainPageViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, UIScrollViewDelegate {

  let idList = ["OverviewController", "ImportantItemsController", "ListMenuController"] // A list of all of my viewControllers' storyboard id
  var currentPage = 1 // Tracking the current page

  override func viewDidLoad() {
    super.viewDidLoad()
    setupPageController()

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

  // UIPageViewControllerDataSource
  func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
    let index = idList.firstIndex(of: viewController.restorationIdentifier!)!
    if (index > 0) {
      return storyboard?.instantiateViewController(withIdentifier: idList[index - 1])
    }
    return nil
  }

  func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
    let index = idList.firstIndex(of: viewController.restorationIdentifier!)!
    if (index < idList.count - 1) {
      return storyboard?.instantiateViewController(withIdentifier: idList[index + 1])
    }
    return nil
  }

  func presentationCount(for pageViewController: UIPageViewController) -> Int {
    return idList.count
  }

  // UIPageViewControllerDelegate
  func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
    if completed {
      guard let vc = pageViewController.viewControllers?.first else { return }
      switch vc {
      case is ImportantItemsController:
          currentPage = 1
      case is OverviewController:
          currentPage = 0
      default:
          currentPage = 2
      }
    }
  }

  // ScrollViewDelegate
  func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let totalViewControllersInPageController = idList.count
    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);
    }
  }

  func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    let totalViewControllersInPageController = idList.count
    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);
    }
  }

  fileprivate func setupPageController() {
    let controller = storyboard?.instantiateViewController(withIdentifier: idList[1]) as! ImportantItemsController // Loading on the second viewController
    setViewControllers([controller], direction: .forward, animated: true, completion: nil)
    dataSource = self
    delegate = self
  }
}

Hassan ElDesouky
  • 334
  • 4
  • 17
1

In case you wish to disable scrollview on a UIPageViewController subclass entirely, you can use the snippet below. Note that this disables not only the bouncing but the horizontal scrolling of the pages as well.

In my case I had a UISegmentedControl to switch between the pages in the PageViewController so disabling scrolling vertically was completely fine and working for me. Hope it helps someone else too.

class MyPageViewController: UIPageViewController {

    private lazy var scrollView: UIScrollView = { view.subviews.compactMap({ $0 as? UIScrollView }).first! }()

    override func viewDidLoad() {
        super.viewDidLoad()

        scrollView.isScrollEnabled = false
    }
}
Peter Ivanics
  • 518
  • 8
  • 13
0

One should implement UIScrollViewDelegate methods to restrict edge scrolls

class MyPageViewController: UIPageViewController {

    override func loadView() {
        super.loadView()
    
        view.subviews.forEach { sv in
            if let scrollView = sv as? UIScrollView {
                scrollView.delegate = self
            }
        }
    }
}

extension MyPageViewController: UIScrollViewDelegate {

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
            
        if currentPage == 0, scrollView.contentOffset.x + scrollView.contentInset.left < 0 {
            scrollView.contentOffset = CGPoint(x: -scrollView.contentInset.left, y: scrollView.contentOffset.y)
        }
        else if currentPage == pageCount - 1, scrollView.contentInset.right < 0, scrollView.contentOffset.x + scrollView.contentInset.right > 0 {
            scrollView.contentOffset = CGPoint(x: -scrollView.contentInset.right, y: scrollView.contentOffset.y)
        }
    }

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

        if currentPage == 0, scrollView.contentOffset.x + scrollView.contentInset.left < 0 {
            targetContentOffset.pointee = CGPoint(x: -scrollView.contentInset.left, y: targetContentOffset.pointee.y)
        }
        else if currentPage == pageCount - 1, scrollView.contentInset.right < 0, scrollView.contentOffset.x + scrollView.contentInset.right > 0 {
            targetContentOffset.pointee = CGPoint(x: -scrollView.contentInset.right, y: targetContentOffset.pointee.y)
        }
    }
}

Here currentPage is the current page index, pageCount is the total page count.

malex
  • 9,874
  • 3
  • 56
  • 77
0

To disable bounces on an UIPageViewController, you can do this (in Swift 5), assuming you declared myPages var as a array of UIViewController :

// Add Scroll delegate

class myController: UIPageViewController, UIScrollViewDelegate { ... }

// Declare currentIdx & scrollview vars

private var currentIdx: Int = 0
private var scrollview: UIScrollView?

// In ViewDidLoad, add delegate & stock scrollview

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

// Retrieve current index like this somewhere in your code

guard let currentViewController = viewControllers?.first, let currentIndex = myPages.firstIndex(of: currentViewController) else {
   return
}

currentIdx = index

// Declare private func to calculate bounces if needed

private func setBounces() {
    let max = myPages.count - 1
    scrollview?.bounces = currentIdx > 0 && currentIdx < max
}

// Add UIScrollViewDelegate methods

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    setBounces()
}

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    setBounces()
}
Medhi
  • 2,656
  • 23
  • 16
-1

UIPageViewController doesn't actually do much for you. You can use a UIScrollView with view controllers quite easily, and disable the bounce on that.

Just do something like

int x=0;
for (NSString *storyboardID in storyboardIDs){
        UIViewController *vc = [storyboard instantiateViewControllerWithIdentifier:storyboardID];
        [self addChildViewController:vc];
        vc.view.frame = CGRectMake(x++*vc.view.frame.size.width, 0, vc.view.frame.size.width, vc.view.frame.size.height);
        [self.scrollView addSubview:vc.view];
        [vc didMoveToParentViewController:self];
        self.scrollView.contentSize = CGSizeMake(storyboardIDs.count*vc.view.frame.size.width, vc.view.frame.size.height);
}
arsenius
  • 12,090
  • 7
  • 58
  • 76
  • True! UIPageViewController makes things much more complicated, the only advantage for basic usage I can see is easier implementation of Page Curl effect than with UIScrollView. Not to say that it's about 10 lines of code with against 100 with the page controller :) – Dimitar Marinov Jun 02 '15 at 11:13
-1

If you will try to disable bounce for UIPageViewController.scrollView, you will definitely get a broken pageViewController: swipe ain't gonna work. So, don't do that:

self.theScrollView.alwaysBounceHorizontal = NO;
self.theScrollView.bounces = NO;

Use the solution with searching scrollView reference in UIPageViewController subviews only for disabling scroll entirely:

@interface MyPageViewController : UIPageViewController
@property (nonatomic, assign) BOOL scrollEnabled;
@end

@interface MyPageViewController ()
@property (nonatomic, weak) UIScrollView *theScrollView;
@end

@implementation MyPageViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    for (UIView *view in self.view.subviews) {
        if ([view isKindOfClass:UIScrollView.class]) {
            self.theScrollView = (UIScrollView *)view;
            break;
        }
    }
}

- (void)setScrollEnabled:(BOOL)scrollEnabled
{
    _scrollEnabled = scrollEnabled;
    self.theScrollView.scrollEnabled = scrollEnabled;
}

@end

Solution for disabling bounce at UIPageViewController:

  1. Create UIScrollView category (for ex. CustomScrolling). UIScrollView is delegate of their gesture recognizer already.
  2. Be aware that your target UIViewController (aka baseVC with UIPageViewController inside) shared via AppDelegate. Otherwise you can use run-time (#import <objc/runtime.h>) and add reference property (to your controller baseVC) to the category.
  3. Implement category:

    @interface UIScrollView (CustomScrolling) <UIGestureRecognizerDelegate>
    @end
    
    @implementation UIScrollView (CustomScrolling)
    
    - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
    {
        UIViewController * baseVC = [(AppDelegate *)[[UIApplication sharedApplication] delegate] baseVC];
        if (gestureRecognizer.view == baseVC.pageViewController.theScrollView) {
            NSInteger page = [baseVC selectedIndex];
            NSInteger total = [baseVC viewControllers].count;
            UIPanGestureRecognizer *recognizer = (UIPanGestureRecognizer *)gestureRecognizer;
            CGPoint velocity = [recognizer velocityInView:self];
            BOOL horizontalSwipe = fabs(velocity.x) > fabs(velocity.y);
            if (!horizontalSwipe) {
                return YES;
            }
            BOOL scrollingFromLeftToRight = velocity.x > 0;
            if ((scrollingFromLeftToRight && page > 0) || (!scrollingFromLeftToRight && page < (total - 1))) {
                return YES;
            }
            return NO;
        }
        return YES;
    }
    
    @end
    
  4. Import category file #import "UIScrollView+CustomScrolling.h" in your baseVC, that uses UIPageViewController.

sig
  • 6,024
  • 2
  • 26
  • 31
  • can you share a gist with entire class? – el.severo Aug 20 '15 at 12:18
  • @el.severo Actually I already did it. There is the only one weakness: get pointer to the `baseVC` inside your `UIScrollView (CustomScrolling)` category. – sig Aug 20 '15 at 12:58
-2

Edit: Do not use this solution. Left here for reference and educational purposes I learned afterwards that this introduces a bug where about 5% of the time, the user can't page in the same direction. They have to page back, then forward again to continue.

If you're using a UIPageViewControllerDataSource, a relatively simple workaround (and a bit hacky) is to disable bouncing each time the pageViewController:viewControllerBeforeViewController: delegate method is called. Here is an example implementation:

@interface YourDataSourceObject ()
@property (strong, nonatomic) UIScrollView *scrollView;
@end

@implementation
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {
    if (!self.scrollView) {
        for (UIView *view in pageViewController.view.subviews) {
            if ([view isKindOfClass:[UIScrollView class]]) {
                self.scrollView = (UIScrollView *)view;
            }
        }
    }
    self.scrollView.bounces = NO;

    // Your other logic to return the correct view controller. 
}
@end
abc123
  • 8,043
  • 7
  • 49
  • 80
-2

This did the trick for me in viewDidLoad:

for subview in pager.view.subviews {
    if let scrollView = subview as? UIScrollView {
        scrollView.bounces = false
        scrollView.alwaysBounceHorizontal = false
        break
    }
}
Tristan Richard
  • 3,385
  • 1
  • 15
  • 17
-2

Working Solution - Swift 5

find the UIScrollView instance from UIPageViewController subviews,

and simply set bounces property to false and you are good to go.

let vc = UIPageViewController(transitionStyle: UIPageViewController.TransitionStyle.scroll, navigationOrientation: UIPageViewController.NavigationOrientation.horizontal, options: [:])
for subview in vc.view.subviews {
  if let scrollView = subview as? UIScrollView {
    scrollView.bounces = false
    break
  }
}
        
Vatsal Shukla
  • 1,274
  • 12
  • 25