31

I am trying to make an effect similar to that found in the new Yahoo weather app. Basically, each page in the UIPageViewController has a background image, and when scrolling through the page view, the Image's location only scrolls about half the speed. How would I do that? I thought I could use some sort of Delegate Method in the UIPageViewController to get the current offset and then update the images like that. The only problem is that I cannot find anyway to tell if the UIPageViewController is being scrolled! Is there a method for that? Thanks!

KlimczakM
  • 12,576
  • 11
  • 64
  • 83
Keiran Paster
  • 600
  • 2
  • 7
  • 21

8 Answers8

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

this gives you access to all standard scroll view API methods. And this is not using private Apple API's.

I added traversing through subviews, to 100% find the UIPageViewController's inner scroll view WARNING: Be careful with scrollview.contentOffset. It resets as the controller scrolls to new pages

If you need persision scrollview offset tracking and stuff like that, it would be better to use a UICollectionViewController with cells sized as the collection view itself and paging enabled.

Ram
  • 3,092
  • 10
  • 40
  • 56
Pavel Gurov
  • 5,587
  • 3
  • 26
  • 23
  • 10
    If Apple changes the architecture of their page view controller you're screwed. – Van Du Tran Jan 10 '14 at 18:49
  • 1
    @VanDuTran agreed, but until then, its all good, and seems like the best approach. I don't see an alternative – Pavel Gurov Jan 25 '14 at 09:47
  • 3
    It wasn't the first subview for me, but the second one. This code did it: `if([self.pageVC.view.subviews[1] respondsToSelector:@selector(setDelegate:)]){ [self.pageVC.view.subviews[1] setDelegate:self]; }` – Whoa Jan 27 '14 at 01:33
  • 8
    The problem with this is that it doesn't give standard results for scrollview.contentOffset. It resets at each new page. – Nick ONeill Feb 13 '14 at 17:53
  • @Paul, well, looks like the alternative was stealing my answer.. :) – Van Du Tran Oct 10 '14 at 20:27
  • 1
    This is dangerous and at risk for crashes when Apple updates the API. UIPageViewController just doesn't work for this sort of thing. I finally just switched over to a UIScrollView. – user3344977 Jan 14 '15 at 23:29
  • Not agree. The only risk is to not find the scrollview since there is a check on the `UIScrollView` type. Btw, this solution is not elegant, the alternative is nicer. – Martin Jul 12 '16 at 07:56
  • 4
    It is not only about chnages by Apple, but you assigns the new delegate, so if the inner implementation of UIPageViewController uses it, you would break that. – Jakub Truhlář Sep 25 '16 at 19:38
  • Could do pageViewController.view.subviews.lazy.compactMap { $0 as? UIScrollView }.first?.delegate = self – Cameron Porter Apr 29 '21 at 03:16
15

I would do this:

Objective-C

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

and implement this protocol

- (void)scrollViewDidScroll:(UIScrollView *)scrollView

Swift

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

and implement this protocol

func scrollViewDidScroll(scrollView: UIScrollView)
Van Du Tran
  • 6,736
  • 11
  • 45
  • 55
3

My guess is that it is not a UIPageViewController, but rather a paged UIScrollView. The UIScrollView does give you a constantly repeated delegate method that tracks what is happening as the scrolling takes place.

Alternatively, you might be able to access the paged UIScrollView that the UIPageViewController is secretly using, but you might break something, and I'm not sure how Apple would feel about it.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 1
    There must be a way to do this with a UIPageViewController. It just simplifies everything if I use it. The Yahoo App looks like it uses a UIPageViewController also. Is there a way to get the position onscreen from within the UIViewController's themselves? – Keiran Paster Apr 28 '13 at 21:47
  • 1
    Isn't a UIPageViewController basically just a fancy UIScrollView? – Keiran Paster Apr 28 '13 at 21:52
  • It may be simpler for you, but I assure you that with UIScrollView alone we were doing stuff like scroll-style UIPageViewController long before scroll-style UIPageViewController existed. There are entire WWDC videos devoted to explaining how to do it. – matt Apr 28 '13 at 21:52
  • It is fancy but opaque. As I said, you can work your way down the view hierarchy and find the scroll view. But you probably shouldn't. – matt Apr 28 '13 at 21:53
  • Is there a way I could put a scroll view on top and just read data from that? – Keiran Paster Apr 28 '13 at 21:55
3

Use @Paul's snippet -

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

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

This gives you the percentage completion of the scroll. Happy coding!

genaks
  • 757
  • 2
  • 10
  • 24
3

In Swift 3 you could write it even shorter:

if let scrollView = self.pageViewController.view.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView {
    scrollView.delegate = self
}
d.felber
  • 5,288
  • 1
  • 21
  • 36
3
extension UIPageViewController {

    var scrollView: UIScrollView? {

        return view.subviews.filter { $0 is UIScrollView }.first as? UIScrollView
    }
}

Using:

pageController.scrollView?.delegate = self
Mike Glukhov
  • 1,758
  • 19
  • 18
1

What you are looking for is called parallax scrolling, you can find several libraries that can help you with that.

Edit: Matt is right this is not an answer, only a hint. Anyway let's complete it:

For animating a background image that lay behind your UIPageViewController you should use the delegate methods that it offer:

-[id<UIPageViewControllerDelegate> pageViewController:willTransitionToViewControllers:]
-[id<UIPageViewControllerDelegate> pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted:]

With these two methods you can calculate the percentage of the scrolling (you should store your controllers in your array to know at which controller you scrolled to and get the percentage)

Antonio E.
  • 4,381
  • 2
  • 25
  • 35
  • 2
    How would you get the percentage from that? – Keiran Paster Apr 28 '13 at 21:58
  • 1
    You should get the position of the searched view controller with `-[NSArray indexOfObject:]` then you can do some basic math to get the percentage of that index respect of the `-[NSArray count]` – Antonio E. Apr 28 '13 at 22:02
  • 1
    That isn't really what I want. Each view controller has its own UIImage inside of it. I need to control the position of the image pixel by pixel, and using a percentage like that would simply not work. – Keiran Paster Apr 28 '13 at 22:03
  • mm probably you can look into `-[UIPageViewController gestureRecognizers]` for a `UIPanGestureRecognizer` and use its methods to know exactly (and legally) what is the current movement. – Antonio E. Apr 28 '13 at 22:16
0

You are not supposed to change the delegate of the page view controller's scroll view: it can break its normal behaviour and/or not be supported later on.

Instead, you can:

  1. Add a pan gesture to the page view controller's view:

    let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panRecognized(gesture:)))
    view.addGestureRecognizer(panGesture)
    panGesture.delegate = self
    
  2. Add the new function in order to know how the view is being scrolled.

    @objc func panRecognized(gesture: UIPanGestureRecognizer) {
        // Do whatever you need with the gesture.translation(in: view)
    }
    
  3. Declare your ViewController as UIGestureRecognizerDelegate.

  4. Implement this function:

    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
    
Tulleb
  • 8,919
  • 8
  • 27
  • 55
  • this causes a problem with swipe back if the page view controller is pushed on a navigation stack (swipe back will be recognised simultaneously with horizontal swiping in the page view controller) – Samuël Dec 29 '22 at 16:19