Unome
Unome

Reputation: 6900

Get scroll position of UIPageViewController

I am using a UIPageViewController, and I need to get the scroll position of the ViewController as the users swipe so I can partially fade some assets while the view is transitioning to the next UIViewController.

The delegate and datasource methods of UIPageViewController don't seem to provide any access to this, and internally I'm assuming that the UIPageViewController must be using a scroll view somewhere, but it doesn't seem to directly subclass it so I'm not able to call

func scrollViewDidScroll(scrollView: UIScrollView) {

}

I've seen some other posts suggestion to grab a reference to the pageViewController!.view.subviews and then the first index is a scrollView, but this seems very hacky. I'm wondering if there is a more standard way to handle this.

Upvotes: 27

Views: 23554

Answers (7)

mojuba
mojuba

Reputation: 12237

For those of us who like functional one-liners and assuming there can only be one scroll view embedded in a page view controller:

view.subviews
    .compactMap { $0 as? UIScrollView }
    .first?.delegate = self

Upvotes: 0

Jack Guo
Jack Guo

Reputation: 4714

As of iOS 13, the UIPageViewController seems to reset the scrollview's contentOffset once it transitions to another view controller. Here is a working solution:

  1. Find the child scrollView and set its delegate to self, as other answers suggested
  2. Keep track of the current page index of the pageViewController:
var currentPageIndex = 0
// The pageViewController's viewControllers
let orderredViewControllers: [UIViewController] = [controller1, controller2, ...]

pageViewController.delegate = self

func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
    guard completed, let currentViewController = pageViewController.viewControllers?.first else { return }
    currentPageIndex = orderredViewControllers.firstIndex(of: currentViewController)!
}
  1. Get the progress that ranges from 0 to 1
func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let contentOffsetX = scrollView.contentOffset.x
    let width = scrollView.frame.size.width
    let offset = CGFloat(currentPageIndex) / CGFloat(orderredViewControllers.count - 1)
    let progress = (contentOffsetX - width) / width + offset
}

Upvotes: 4

D6mi
D6mi

Reputation: 619

To make the code as readable and separated as possible, I would define an extension on UIPageViewController:

extension UIPageViewController {
  var scrollView: UIScrollView? {
    view.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView
  }
}

It's quite easy to set yourself as the delegate for scroll view events, as so:

pageViewController.scrollView?.delegate = self

Upvotes: 2

Maxim Golovlev
Maxim Golovlev

Reputation: 117

var pageViewController: PageViewController? {
    didSet {
        pageViewController?.dataSource = self
        pageViewController?.delegate = self
        scrollView?.delegate = self
    }
}

lazy var scrollView: UIScrollView? = {
    for subview in pageViewController?.view?.subviews ?? [] {
        if let scrollView = subview as? UIScrollView {
            return scrollView
        }
    }
    return nil
}()

extension BaseFeedViewController: UIScrollViewDelegate {

func scrollViewDidScroll(_ scrollView: UIScrollView) {

    let offset = scrollView.contentOffset.x
    let bounds = scrollView.bounds.width
    let page = CGFloat(self.currentPage)
    let count = CGFloat(viewControllers.count)
    let percentage = (offset - bounds + page * bounds) / (count * bounds - bounds)

    print(abs(percentage))
}
}

Upvotes: 3

Ashkan Ghodrat
Ashkan Ghodrat

Reputation: 3222

UIPageViewController scroll doesn't work like normal scrollview and you can't get scrollView.contentOffset like other scrollViews.

so here is a trick to get what's going on when user scrolls :

first you have to find scrollview and set delegate to current viewController like other answers said.

class YourViewController : UIPageViewController {

    var startOffset = CGFloat(0) //define this

    override func viewDidLoad() {
         super.viewDidLoad()

         //from other answers   
         for v in view.subviews{
             if v is UIScrollView {
                 (v as! UIScrollView).delegate = self
             }
         }
     }

    .
    .
    .
}

extension YourViewController : UIScrollViewDelegate{

    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {

        startOffset = scrollView.contentOffset.x
    }

    public func scrollViewDidScroll(_ scrollView: UIScrollView) {

        var direction = 0 //scroll stopped

        if startOffset < scrollView.contentOffset.x {
            direction = 1 //going right
        }else if startOffset > scrollView.contentOffset.x {
            direction = -1 //going left
        }

        let positionFromStartOfCurrentPage = abs(startOffset - scrollView.contentOffset.x)
        let percent = positionFromStartOfCurrentPage /  self.view.frame.width

        //you can decide what to do with scroll
    }

}

Upvotes: 19

Christian
Christian

Reputation: 22343

You can search for the UIScrollView inside your UIPageViewController. To do that, you will have to implement the UIScrollViewDelegate.

After that you can get your scrollView:

for v in pageViewController.view.subviews{
    if v.isKindOfClass(UIScrollView){
        (v as UIScrollView).delegate = self
    }
}

After that, you are able to use all the UIScrollViewDelegate-methods and so you can override the scrollViewDidScroll method where you can get the scrollPosition:

func scrollViewDidScroll(scrollView: UIScrollView) {
   //your Code
}

Or if you want a one-liner:

let scrollView = view.subviews.filter { $0 is UIScrollView }.first as! UIScrollView
scrollView.delegate = self

Upvotes: 45

jeppeb
jeppeb

Reputation: 528

Similar to Christian's answer but a bit more Swift-like (and not unnecessarily continuing to loop through view.subviews):

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

Upvotes: 10

Related Questions