Detecting UIScrollView page change
Asked Answered
S

10

82

Is there a way to detect or get a notification when user changes the page in a paging-enabled UIScrollView?

Shoveler answered 11/3, 2011 at 11:18 Comment(0)
A
17

Implement the delegate of UIScrollView. This method is what you are looking for.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
Ailurophobe answered 11/3, 2011 at 11:20 Comment(6)
detecting pagechange in this method is not efficient, because the code will be run everytime the user swipe. So it is better to do it in scrollViewDidEndDecelerating method.Burbage
@Burbage - This solution has a problem: if user stops scrolling exactly at the end of the page (this is quite possible), this function won't be called!Catheterize
@MaciejKozieł did you find a solution for that issue?Recrystallize
@Recrystallize Yes, I stopped worrying about it ;) I can think of few alternatives (and original answer, while less efficient, is one of them). I needed it for page indicator, and this edge case isn't app breaking problem; it will refresh as soon as any deceleration occurs.Catheterize
@Burbage you solved a very specific issue for me with that comment. Thank you.Hauptmann
Guys, you're talking about doing maybe TWO ("2") math calculations on each step of didScroll. It would be utterly impossible to measure such a small amount of processing power. Note that the cpu is doing millions of calculations a frame when an iPhone is merely ticking over doing nothing! (When scrolling, for goodness sake, there is a HUGE amount of processing happening on the cpu and gpu, good grief!) There is NO performance concern.Rabblerouser
A
194

Use this to detect which page is currently being shown and perform some action on page change:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    static NSInteger previousPage = 0;
    CGFloat pageWidth = scrollView.frame.size.width;
    float fractionalPage = scrollView.contentOffset.x / pageWidth;
    NSInteger page = lround(fractionalPage);
    if (previousPage != page) {
        // Page has changed, do your thing!
        // ...
        // Finally, update previous page
        previousPage = page;
    }
}

If it's acceptable for you to only react to the page change once the scrolling has completely stopped, then it would be best to do the above inside the scrollViewDidEndDecelerating: delegate method instead of the scrollViewDidScroll: method.

Abmho answered 11/3, 2011 at 11:32 Comment(5)
I know this is old but maybe it'll help, using this method can return negative numbers and also pages that are out of the bounds of the scrollview. If you are using the page to access an array this can obviously cause problems and therefore checks may be needed on the page.Pollster
writing the above code inside this would be much better... - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollViewArianearianie
I don't understand why this answer is being up voted. Using the above will be called on every movment of the scroll view. There's really no need for that. Using scrollViewDidEndDecelerating is a better solution but not a complete one. I've added a solution of my own that solved it completely for me.Coper
It depends on when they want to react to a page change. If they only need to know when the scrollview has stopped scrolling then doing it after decelerating is best. Otherwise, the did scroll method is best.Abmho
@segev - scrollViewDidEndDecelerating does not work.Rabblerouser
S
66

In paging enabled scroll view you can use scrollViewDidEndDecelerating to know when the view is settled on a page (might be the same page).

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

scrollViewDidScroll gets called on every movement. And in context of paging enabled view can be used to find when it is scrolled enough to move to next page (if dragging is stopped at that point).

Sophronia answered 7/4, 2012 at 16:1 Comment(3)
This solution has a problem: if user stops scrolling exactly at the end of the page (this is quite possible), this function won't be called!Catheterize
@MaciejKozieł A better solution is giving in another answer by meCoper
@Sagev How does it solve that case where - (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView and - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView are not called at all?Catheterize
F
48

How about combining two methods of UIScrollViewDelegate?

In scrollViewDidEndDragging(_:willDecelerate:), if it stops right away, we do the page calculation; if it is decelerating, we let it go and it will be caught by scrollViewDidEndDecelerating(_:).

The code is tested with XCode version 7.1.1, Swift version 2.1

class ViewController: UIViewController, UIScrollViewDelegate {

  // MARK: UIScrollViewDelegate
  func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    if decelerate == false {
        let currentPage = scrollView.currentPage
        // Do something with your page update
        print("scrollViewDidEndDragging: \(currentPage)")
    }
  }

  func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
    let currentPage = scrollView.currentPage
    // Do something with your page update
    print("scrollViewDidEndDecelerating: \(currentPage)")
  }

}

extension UIScrollView {
   var currentPage: Int {
      return Int((self.contentOffset.x+ (0.5*self.frame.size.width))/self.frame.width)+1
   }
}
Forby answered 19/11, 2015 at 15:33 Comment(1)
This won't work when you have both vertical and horizontal gestures enabled.Hadden
C
25

First result on google for page detection so I had to answer this with a better solution in my opinion. (even if this question was asked 2 and a half years ago.)

I'd prefer not to call scrollViewDidScroll just to track the page number. Thats an overkill for something simple as that. Using scrollViewDidEndDecelerating does work and stops on a page change BUT (and it's a big but) if the user will swipe on the screen twice a bit faster than normal scrollViewDidEndDecelerating will be called only once. You can easily go from page #1 to page #3 without processing page #2.

This solved it completely for me:

- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
{
    scrollView.userInteractionEnabled = NO;
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    //Run your code on the current page
    scrollView.userInteractionEnabled = YES;
}

That way the user can only swipe one page at a time without the risk described above.

Coper answered 16/12, 2013 at 12:53 Comment(8)
You are totally right. This is a good solution for infinite scroll.Benzoic
If the user tries to swipe multiple pages in quick succession, it won't behave as expected. However, while this isn't perfect, it's a quick fix for me until I have more time to devote to a better solution.Crapulous
@Pier-LucGendreau As fas as my tests go there is no such scenario. The user can't swipe twice. Put a break point inside scrollViewWillBeginDecelerating and you will learn that the mil second the user lifts his finger from the first swipe you'll reach that break point.Coper
I know, but if you swipe again before the first swipe animation finishes, that second swipe is lost.Crapulous
@Pier-LucGendreau You can't swipe again until scrollViewDidEndDecelerating is triggered. That's the point of this fix.Coper
Thank you, kind sir, for taking responsibility in improving an old answer.Dissipate
I came up with the same solution, its not perfect for UX but its pretty stable ;)Embrangle
I realize the same issue that users may scroll too fast and scrollViewDidEndDecelerating will not be called. I thin your approach is good. Thanks !Frederico
C
19

Swift 4

I found the best way to do this is by using scrollViewWillEndDragging(_:withVelocity:targetContentOffset:). It lets you predict if paging will occur as soon as you lift your finger off the screen. This example is for paging horizontally.

Remember to the assign the scrollView.delegate to the object that adopts UIScrollViewDelegate and implements this method.

var previousPageXOffset: CGFloat = 0.0

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

    let targetOffset = targetContentOffset.pointee

    if targetOffset.x == previousPageXOffset {
        // page will not change
    } else if targetOffset.x < previousPageXOffset {
        // scroll view will page left
    } else if targetOffset.x > previousPageXOffset {
        // scroll view will page right
    }

    previousPageXOffset = targetOffset.x
    // If you want to track the index of the page you are on just just divide the previousPageXOffset by the scrollView width.
    // let index = Int(previousPageXOffset / scrollView.frame.width)


}
Cherlycherlyn answered 27/3, 2018 at 14:24 Comment(1)
That's the correct answer, the other methods are called even if a scrollView don't scroll sufficiently to trigger the paging change.Smorgasbord
A
17

Implement the delegate of UIScrollView. This method is what you are looking for.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
Ailurophobe answered 11/3, 2011 at 11:20 Comment(6)
detecting pagechange in this method is not efficient, because the code will be run everytime the user swipe. So it is better to do it in scrollViewDidEndDecelerating method.Burbage
@Burbage - This solution has a problem: if user stops scrolling exactly at the end of the page (this is quite possible), this function won't be called!Catheterize
@MaciejKozieł did you find a solution for that issue?Recrystallize
@Recrystallize Yes, I stopped worrying about it ;) I can think of few alternatives (and original answer, while less efficient, is one of them). I needed it for page indicator, and this edge case isn't app breaking problem; it will refresh as soon as any deceleration occurs.Catheterize
@Burbage you solved a very specific issue for me with that comment. Thank you.Hauptmann
Guys, you're talking about doing maybe TWO ("2") math calculations on each step of didScroll. It would be utterly impossible to measure such a small amount of processing power. Note that the cpu is doing millions of calculations a frame when an iPhone is merely ticking over doing nothing! (When scrolling, for goodness sake, there is a HUGE amount of processing happening on the cpu and gpu, good grief!) There is NO performance concern.Rabblerouser
F
2

Here is the swift solution for this.

Make two properties currentPage and previousPage in the class where you are implementing your code and initialize them to 0.

Now update currentPage from scrollViewDidEndDragging(:willDecelerate:) and scrollViewDidEndDecelerating(:scrollView:).

And then update previousPage in scrollViewDidEndScrollingAnimation(_:scrollView:)

    //Class Properties
    var currentPage = 0
    var previousPage = 0

func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        updatePage(scrollView)
        return
    }

 func scrollViewDidEndDecelerating(scrollView: UIScrollView){
        updatePage(scrollView)
        return
    }


 func updatePage(scrollView: UIScrollView) {
        let pageWidth:CGFloat = scrollView.frame.width
        let current:CGFloat = floor((scrollView.contentOffset.x-pageWidth/2)/pageWidth)+1
        currentPage = Int(current)

        if currentPage == 0 {
              // DO SOMETHING
        }
        else if currentPage == 1{
              // DO SOMETHING

        }
    }

func scrollViewDidEndScrollingAnimation(scrollView: UIScrollView) {
     if previousPage != currentPage {
          previousPage = currentPage
          if currentPage == 0 {
              //DO SOMETHING
             }else if currentPage == 1 {
               // DO SOMETHING
           }
       }
   }
Foothold answered 22/3, 2017 at 10:25 Comment(0)
L
2
var scrollViewPage = 0
override func viewDidLoad() {
    super.viewDidLoad()
    scrollViewPage = scrollView.currentPage
}

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    if scrollViewPage != scrollView.currentPage {
        scrollViewPage = scrollView.currentPage
        // Do something with your page update
        print("scrollViewDidEndDecelerating: \(scrollViewPage)")
    }
}

And Use extension

extension UIScrollView {
    var currentPage: Int {
        return Int((self.contentOffset.x + (0.5 * self.frame.size.width)) / 
        self.frame.width) + 1
    }
}
Laurettalaurette answered 11/9, 2020 at 10:28 Comment(0)
R
1

Cut and paste for 2019

It's not so easy to do this:

var quantumPage: Int = -100 {   // the UNIQUELY LANDED ON, NEVER REPEATING page
    didSet {
        print(">>>>>> QUANTUM PAGE IS \(quantumPage)")
        pageHasActuallyChanged() // your function
    }
}

private var possibleQuantumPage: Int = -100 {
    didSet {
        if oldValue != possibleQuantumPage {
            quantumPage = possibleQuantumPage
        }
    }
}

public func scrollViewDidEndDragging(
                    _ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    if decelerate == false {
        possibleQuantumPage = currentPageEvenIfInBetween
    }
}

public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    possibleQuantumPage = currentPageEvenIfInBetween
}

var currentPageEvenIfInBetween: Int {
   return Int((self.contentOffset.x + (0.5 * self.frame.width)) / self.frame.width)
}

Works perfectly.

pageHasActuallyChanged will only be called when the user changes pages in what humans would consider "changing pages".

Be aware: the bringup is tricky:

This is difficult to initialize at bringup time, and will depend on how you are using the paged system.

In any paged system you will very likely have something like "scrollToViewAtIndex..."

open func scrollToViewAtIndexForBringup(_ index: Int) {
    if index > -1 && index < childViews.count {
        
        let w = self.frame.size.width
        let h = self.frame.size.height
        
        let frame = CGRect(x: CGFloat(index)*w, y: 0, width: w, height: h)
        scrollRectToVisible(frame, animated: false) // NOTE THE FALSE
        
        // AND IMPORTANTLY:
        possibleQuantumPage = currentPageEvenIfInBetween
    }
}

So, if the user opens the "book" at page 17, in your boss class you'd be calling that function to set it to "17" on bringup.

In such an example, you'd just have to remember that you must set initially our possibleQuantumPage value in any such bringup functions; there's no really generalized way to handle the starting situation.

After all you may, just for example, want to "quickly scroll" to the bringup page, and, who knows what that "means" in a quantumPage situation. So, be sure to initialize your quantum page system carefully during bringup, based on your situation.

In any event, just copy and paste the five functions at the top to get perfect quantum paging.

Rabblerouser answered 26/11, 2019 at 18:13 Comment(0)
F
0

For Swift

static var previousPage: Int = 0
func scrollViewDidScroll(_ scrollView: UIScrollView){
    let pageWidth: CGFloat = scrollView.frame.width
    let fractionalPage: CGFloat = scrollView.contentOffset.x / pageWidth
    let page = lround(Double(fractionalPage))
    if page != previousPage{
        print(page)
        // page changed
    }
}
Faddish answered 26/10, 2018 at 12:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.