问题
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
?
回答1:
Disable UIPageViewController's bounce
Add the
<UIScrollViewDelegate>
delegate to your UIPageViewController's headerSet 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; } }
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); } }
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);
}
}
回答2:
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)
}
}
回答3:
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
}
}
}
回答4:
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);
}
回答5:
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.
回答6:
Edit: Do not use this solution. 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
回答7:
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:
- Create
UIScrollView
category (for ex. CustomScrolling).UIScrollView
is delegate of their gesture recognizer already. - Be aware that your target
UIViewController
(akabaseVC
withUIPageViewController
inside) shared viaAppDelegate
. Otherwise you can use run-time (#import <objc/runtime.h>
) and add reference property (to your controllerbaseVC
) to the category. 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
Import category file
#import "UIScrollView+CustomScrolling.h"
in yourbaseVC
, that uses UIPageViewController.
回答8:
@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)
}
}
回答9:
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:
- Let's assume we have a
UIViewController
whereUIPageViewController
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
}
- Set
ViewController
as delegate ofUIPageViewController
:
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
}
}
- Set
ViewController
as datasource ofUIPageController
:
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
}
}
- "Disable" bouncing by setting
ViewController
as delegate ofUIPageViewController
'sUIScrollView
:
// 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")
}
}
}
来源:https://stackoverflow.com/questions/21798218/disable-uipageviewcontroller-bounce