禁用UIPageViewController的弹性效果

58

我搜索了很多,但还没有找到合适的解决方案。

是否可能禁用UIPageViewController的弹跳效果,并仍然使用UIPageViewControllerTransitionStyleScroll


1
如果你想要去掉那个弹跳效果,你可以使用UIPageViewControllerTransitionStylePageCurl样式。 - Pawan Rai
1
@pawan 我想使用UIPageViewControllerTransitionStyleScroll。 - Mario
17个回答

84

禁用 UIPageViewController 的弹跳效果

  1. <UIScrollViewDelegate> 代理添加到 UIPageViewController 的头文件中。

  2. viewDidLoad 中将 UIPageViewController 的基础 UIScrollView 的代理设置为其父级代理:

  3. for (UIView *view in self.view.subviews) {
        if ([view isKindOfClass:[UIScrollView class]]) {
            ((UIScrollView *)view).delegate = self;
            break;
        }
    }
    
  4. scrollViewDidScroll 方法的实现是在用户滑出边界时将 contentOffset 重置到原点(而不是(0,0),而是 (bound.size.width,0)),代码示例如下:

  5. - (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);
        }
    }
    
  6. 最后,scrollViewWillEndDragging 函数是为了解决一个bug场景:当用户在第一页快速从左向右滑动时,由于上面的函数,第一页不会在左边弹跳,但会在右边(可能是)滑动的速度引起的情况下弹跳。最后,当被弹回时,UIPageViewController 将触发页面翻转到第二页(当然,这是不期望的)。

  7. - (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

viewDidLoad中插入的代码:

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

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

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

4
@DongMa,“isPageToBounce”和“_currentPage”应该是什么类型?我很难理解这段代码,请您逐步解释一下。我认为这是网络上唯一的解决此问题的方法(已经找了几个小时了)。 - apolo
1
谢谢 ;) 我试图解决这个问题太久了 :) - pasql
@DongMa,我该如何使页面滑动调用这两个委托。我尝试在VC中进行了尝试,其中PageVC被嵌入,并在页面视图控制器中进行了第二次尝试。实际问题是我的整个页面都在滚动视图中,其中大约有200个点的高度被PageVC占用。所以我真的不知道该怎么做,任何建议都将非常有帮助。 - zeal
@zeal 你是在说设置底层scrollview的代理吗?我在我的答案中指出了这一点:“要实现代理,你可以:”,并给出了两个解决方案。详细信息可以在其他问题中找到,比如这个:http://stackoverflow.com/a/29023923/1016214 - Dong Ma
10
解决方案不完整,请添加更多说明以跟踪当前页面的方法。 - Benzy
显示剩余8条评论

18

禁用UIPageViewController的弹跳效果

Swift 2.2

补充答案

1)将UIScrollViewDelegate添加到UIPageViewController中

extension PageViewController: UIScrollViewDelegate

2) 添加到viewDidLoad中

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

3) 添加UIScrollViewDelegate方法

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

很好地工作了,你只需要正确设置currentIndex! - Balázs Papp
一开始我不确定如何正确设置索引,最终按照我在答案中发布的方式进行了操作。如果您想包含它,我可以删除。 - mwright
@BalázsPapp,非常同意你的观点。 - Vatsal Shukla
@BalázsPapp 你是如何正确设置currentIndex的呢? - Niloy Mahmud
1
请查看此帖子以在UIPageViewController中找到currentIndex:https://dev59.com/questions/32oy5IYBdhLWcg3wq_wk - bmjohns
当我在一个PageViewController中有三个视图控制器时,它无法正常工作。 - Istiak Morsalin

5

唯一可行的解决方案,2020

以下是一个完整的解决方案,与其他答案不同的是,它不需要您编写/测试自己的代码来跟踪当前显示页面的索引。

假设您的页面存储在不可变数组 sourcePageViewControllers 中,并且在创建了 UIPageViewController 作为 myPageViewController之后:

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

然后:

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

}

4
如果你有追踪currentIndex的记录,下面的代码就足够了,但是它有一些小bug,因为有一种随机情况会完全停止滚动。
我认为scrollView.bounces有一些小问题,也许我漏掉了什么,因为大部分时间它都能正常工作。如果有人能够根据下面的代码提供解决方案,那就太好了。
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
    scrollView.bounces = currentIndex == 0 ||
        currentIndex == controllers.count - 1
        ? false 
        : true
}

多好又简单的解决方案!更简单地说:scrollView.bounces = !(currentIndex == .zero || currentIndex == controllers.count - 1) - jason d

1
@Dong Ma的方法很完美,但还可以进一步改进和简化。
放入viewDidLoad中的代码:
for subview in view.subviews {
    if let scrollView = subview as? UIScrollView {
        scrollView.delegate = self
        break
    }
}

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

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

1

我不确定如何正确管理currentIndex,但最终做了这样的处理

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

1
当前索引?问题是关于如何防止反弹。 - Zaporozhchenko Oleksandr
@ZaporozhchenkoAleksandr,如果不使用当前索引,你无法防止反弹。 - atereshkov

1

另一个选择是将ScrollView.bounce设置为false。 这解决了我的pageViewController问题(当然不是关于ScrollView的问题)。 弹跳被禁用,所有页面都可以滚动而无需反弹。


1
这是错误的建议 - 如果UIPageViewController的滚动视图禁用了弹跳,它将停止水平分页。 - thefaj

1
如果您希望完全禁用UIPageViewController子类上的scrollview,则可以使用下面的代码片段。请注意,这不仅禁用弹跳,还禁用页面的水平滚动。
在我的情况下,我有一个UISegmentedControl来在PageViewController中切换页面,因此垂直滚动被完全禁用并对我有效。希望它也能帮助其他人。
class MyPageViewController: UIPageViewController {

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

    override func viewDidLoad() {
        super.viewDidLoad()

        scrollView.isScrollEnabled = false
    }
}

1

以下是Dong Ma编辑的答案,其中:

  • 新增 - 尊重布局方向(例如希伯来语)
  • 修复 - 当快速滑动时计数错误的currentIndex

信息:

  • 使用Swift 5.0编写
  • 在Xcode 10.2.1中构建和测试
  • iOS 12.0

如何操作:

  1. 假设我们有一个UIViewController,其中添加了UIPageViewController作为子VC。
class ViewController: UIViewController {
    var pageNavigationController: UIPageViewController! 

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

    // rest of UI's setups  
}
  1. ViewController 设置为 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. ViewController设置为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
    }
}

通过将ViewController设置为UIPageViewControllerUIScrollView的代理来“禁用”弹跳效果:
// 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")
        }
    }
}

1

我的解决方案是使用Swift 5
在我的场景中,我首先在第二页加载UIPageViewController。我有三个页面,因此我从中间打开。

这是我的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
  }
}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接