Disable UIPageViewController bounce

前端 未结 13 2383
离开以前
离开以前 2020-11-27 04:08

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

相关标签:
13条回答
  • 2020-11-27 04:38

    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
            }
        }
    
    }
    
    0 讨论(0)
  • 2020-11-27 04:43

    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")
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-27 04:44

    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);
    }
    
    0 讨论(0)
  • 2020-11-27 04:44

    @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)
        }
      }
    
    0 讨论(0)
  • 2020-11-27 04:45

    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
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-27 04:51

    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
      }
    }
    
    
    0 讨论(0)
提交回复
热议问题