15

I would like to receive updates from the uipageviewcontroller during the page scrolling process. I want to know the transitionProgress in %. (This value should update when the user move the finger in order to get to another page). I'm interested in the animation progress from one page to another, not the progress through the total number of pages.

What I have found so far:

  • There is a class called UICollectionViewTransitionLayout that have the property corresponding to what I am looking for, "transitionProgress". Probably uipageviewcontroller implement this method somehow?

  • I can call the following method on the uipagecontroller but I only get 0 as result!

    CGFloat percentComplete = [self.pageViewController.transitionCoordinator percentComplete];

Onato
  • 9,916
  • 5
  • 46
  • 54
Eric1101000101
  • 531
  • 3
  • 6
  • 15
  • I assume you've seen the [delegate protocol](https://developer.apple.com/library/ios/documentation/uikit/reference/UIPageViewControllerDelegateProtocolRef/UIPageViewControllerDelegate.html#//apple_ref/occ/intfm/UIPageViewControllerDelegate/pageViewController:willTransitionToViewControllers:)? – aroth Mar 22 '14 at 13:04
  • Yes, I use the delegate protocol to get the start and end point of the transition. – Eric1101000101 Mar 22 '14 at 13:10

7 Answers7

26

in SWIFT to copy paste ;) works perfect for me

extension UIPageViewController: UIScrollViewDelegate {

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

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

   public func scrollViewDidScroll(_ scrollView: UIScrollView) {
       let point = scrollView.contentOffset
       var percentComplete: CGFloat
       percentComplete = abs(point.x - view.frame.size.width)/view.frame.size.width
       print("percentComplete: ",percentComplete)
   }
}
Husam
  • 8,149
  • 3
  • 38
  • 45
Appygix
  • 642
  • 12
  • 26
  • 1
    You could write it a little bit better in terms of Swift: for view in self.view.subviews { if let scrollView = view as? UIScrollView { scrollView.delegate = self } } – user464230 Jul 05 '17 at 11:45
  • Here is a functional one line version that avoids the for loop: (view.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView)?.delegate = self – dfmuir Jul 12 '21 at 21:24
4

At last I found out a solution, even if it is probably not the best way to do it:

I first add an observer on the scrollview like this:

// Get Notified at update of scrollview progress
NSArray *views = self.pageViewController.view.subviews;
UIScrollView* sW = [views objectAtIndex:0];
[sW addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:NULL];

And when the observer is called:

NSArray *views = self.pageViewController.view.subviews;
UIScrollView* sW = [views objectAtIndex:0];
CGPoint point = sW.contentOffset;

float percentComplete;
//iPhone 5
if([ [ UIScreen mainScreen ] bounds ].size.height == 568){
    percentComplete = fabs(point.x - 568)/568;

} else{
//iphone 4
    percentComplete = fabs(point.x - 480)/480;
}
NSLog(@"percentComplete: %f", percentComplete);

I'm very happy that I found this :-)

Eric1101000101
  • 531
  • 3
  • 6
  • 15
  • 1
    I have updated the reply. Hope it helps man. Sorry I did not see your question unril now. – Eric1101000101 Jan 28 '16 at 02:26
  • Just an FYI, if you can ever avoid using KVO, avoid using KVO. The method mentioned in the answer noting to listen in for the scrollview delegate is far more appropriate, this is guaranteed to give you a proper callback and is not stringly based code relying on ObjC mechanisms. – TheCodingArt Jan 28 '16 at 02:38
  • 1
    What if someone else was delegating to scroll view (or Apple decides to assign a delegate to it in iOS 11?)? So no, KVO approach is better IMO. But I must add, all these solutions are risky. Apple might decide to change the view hierarchy of UIPageViewController and all these solutions are garbage. – batu Oct 25 '16 at 06:40
2

Since I thought that the functionality of scrolling would stay forever, but that the internal implementation may change to something other than a scroll view, I found the solution below (I haven't tested this very much, but still)

NSUInteger offset = 0;
UIViewController * firstVisibleViewController;
while([(firstVisibleViewController = [self viewControllerForPage:offset]).view superview] == nil) {
  ++offset;
}
CGRect rect = [[firstVisibleViewController.view superview] convertRect:firstVisibleViewController.view.frame fromView:self.view];
CGFloat absolutePosition = rect.origin.x / self.view.frame.size.width;
absolutePosition += (CGFloat)offset;

(self is the UIPageViewController here, and [-viewControllerForPage:] is a method that returns the view controller at the given page)

If absolutePosition is 0.0f, then the first view controller is shown, if it's equal to 1.0f, the second one is shown, etc... This can be called repeatedly in a CADisplayLink along with the delegate methods and/or UIPanGestureRecognizer to effectively know the status of the current progress of the UIPageViewController.

EDIT: Made it work for any number of view controllers

Stéphane Copin
  • 1,888
  • 18
  • 18
2

Use this -

for (UIView *v in self.pageViewController.view.subviews) {
    if ([v isKindOfClass:[UIScrollView class]]) {
        ((UIScrollView *)v).delegate = self;
    }
}

to implement this protocol : -(void)scrollViewDidScroll:(UIScrollView *)scrollView

and then use @xhist's code (modified) in this way

-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGPoint point = scrollView.contentOffset;

float percentComplete;
percentComplete = fabs(point.x - self.view.frame.size.width)/self.view.frame.size.width;
NSLog(@"percentComplete: %f", percentComplete);
}
genaks
  • 757
  • 2
  • 10
  • 24
2

Based on Appgix solution, I'm adding this directly on my 'UIPageViewController' subclass. (Since I only need it on this one)

For Swift 3:

class MYPageViewControllerSubclass: UIPageViewController, UIScrollViewDelegate {

   override func viewDidLoad() {
          super.viewDidLoad()

          for subView in view.subviews {
             if subView is UIScrollView {
                (subView as! UIScrollView).delegate = self                
             }
          }
    }

    // MARK: - Scroll View Delegate

    public func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let point = scrollView.contentOffset
        var percentComplete: CGFloat
        percentComplete = fabs(point.x - view.frame.size.width)/view.frame.size.width
        NSLog("percentComplete: %f", percentComplete)
    }

    // OTHER CODE GOES HERE...

}
Pochi
  • 13,391
  • 3
  • 64
  • 104
1

While Appgix' solution seemed to work at first, I noticed that when the user pans in a UIPageViewController, lifts the finger shortly and then immediately starts dragging again while the "snap-back" animation is NOT YET finished and then lifts his finger again (which will again "snap-back"), the scrollViewDidScroll method is only called when the page view controller finished the animation. For the progress calculation this means the second pan produces continuous values like 0.11, 0.13, 0.16 but when the scroll view snaps back the next progress value will be 1.0 which causes my other scroll view to be out of sync.

To fight this I'm now listening to the scroll view's contentOffset key, which is still updated continuously in this situation.

fruitcoder
  • 1,073
  • 8
  • 24
0

KVO approach for Swift 4

var myContext = 0

override func viewDidLoad() {
    for view in self.view.subviews {
        if view is UIScrollView {
            view.addObserver(self, forKeyPath: "contentOffset", options: .new, context: &introPagingViewControllerContext)
        }
    }
}

// MARK: KVO

override func observeValue(forKeyPath keyPath: String?,
                           of object: Any?,
                           change: [NSKeyValueChangeKey : Any]?,
                           context: UnsafeMutableRawPointer?)
{
    guard let change = change else { return }
    if context != &myContext {
        super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        return
    }

    if keyPath == "contentOffset" {
        if let contentOffset = change[NSKeyValueChangeKey.newKey] as? CGPoint {
            let screenWidth = UIScreen.main.bounds.width
            let percent = abs((contentOffset.x - screenWidth) / screenWidth)
            print(percent)
        }
    }
}
SamB
  • 2,621
  • 4
  • 34
  • 39