172

UIScrollViewDelegate has got two delegate methods scrollViewDidScroll: and scrollViewDidEndScrollingAnimation: but neither of these tell you when scrolling has completed. scrollViewDidScroll only notifies you that the scroll view did scroll not that it has finished scrolling.

The other method scrollViewDidEndScrollingAnimation only seems to fire if you programmatically move the scroll view not if the user scrolls.

Does anyone know of scheme to detect when a scroll view has completed scrolling?

3

22 Answers 22

183

The 320 implementations are so much better - here is a patch to get consistent start/ends of the scroll.

-(void)scrollViewDidScroll:(UIScrollView *)sender 
{   
    [NSObject cancelPreviousPerformRequestsWithTarget:self];
    //ensure that the end of scroll is fired.
    [self performSelector:@selector(scrollViewDidEndScrollingAnimation:) withObject:sender afterDelay:0.3]; 

...
}

-(void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
{
    [NSObject cancelPreviousPerformRequestsWithTarget:self];
...
}
12
  • 4
    should be [self performSelector:@selector(scrollViewDidEndScrollingAnimation:) withObject:sender afterDelay:0.3]
    – Brainware
    Commented Jul 21, 2013 at 2:19
  • +1 I needed to use a custom paging metric (less than view width) and the animation would snap to different positions randomly (with the same end X positions), and this fixed it.
    – munch1324
    Commented Sep 30, 2013 at 13:32
  • I wonder how can this be achieved using swift given that there is no support for the performSelector methods in swift. Any ideas? Commented Dec 22, 2014 at 21:27
  • 2
    I honestly can't believe I'm in Feb 2016, iOS 9.3 and this is still the best solution to this problem. Thanks, worked like a charm
    – gmogames
    Commented Feb 5, 2016 at 20:23
  • 1
    I believe delay 0.3 is unnecessary and 0.0 is better. The selector won't be performed until the runloop leaves tracking mode.
    – malhal
    Commented Oct 29, 2020 at 10:28
175
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    [self stoppedScrolling];
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    if (!decelerate) {
        [self stoppedScrolling];
    }
}

- (void)stoppedScrolling {
    // ...
}
4
  • 16
    It looks like that only works if it is decelerating though. If you pan slowly, then release, it doesn't call didEndDecelerating. Commented Nov 1, 2010 at 23:00
  • 5
    Though it's the official API. It actually doesn't always work as we expect. @Ashley Smart gave a more practical solution.
    – Di Wu
    Commented Jun 14, 2011 at 8:45
  • Works perfectly and the explanation makes sense Commented Dec 18, 2012 at 23:59
  • This works only for scroll that are due to dragging interaction. If your scroll is due to something else (like keyboard opening or keyboard closing), it seems like you'll have to detect the event with a hack, and scrollViewDidEndScrollingAnimation is not useful either. Commented Oct 15, 2013 at 10:32
24

This has been described in some of the other answers, but here's (in code) how to combine scrollViewDidEndDecelerating and scrollViewDidEndDragging:willDecelerate to perform some operation when scrolling has finished:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    [self stoppedScrolling];
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView 
                  willDecelerate:(BOOL)decelerate
{
    if (!decelerate) {
        [self stoppedScrolling];
    }
}

- (void)stoppedScrolling
{
    // done, do whatever
}
21

For all scrolls related to dragging interactions, this will be sufficient:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    _isScrolling = NO;
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    if (!decelerate) {
        _isScrolling = NO;
    }
}

Now, if your scroll is due to a programmatic setContentOffset/scrollRectVisible (with animated = YES or you obviously know when scroll is ended):

 - (void)scrollViewDidEndScrollingAnimation {
     _isScrolling = NO;
}

If your scroll is due to something else (like keyboard opening or keyboard closing), it seems like you'll have to detect the event with a hack because scrollViewDidEndScrollingAnimation is not useful either.

The case of a PAGINATED scroll view:

Because, I guess, Apple apply an acceleration curve, scrollViewDidEndDecelerating get called for every drag so there's no need to use scrollViewDidEndDragging in this case.

1
  • Your paginated scroll view case have one issue: if you release scrolling exactly at page end position (when no scroll needed), scrollViewDidEndDecelerating will not be called
    – Yury
    Commented Aug 5, 2019 at 16:19
20

I think scrollViewDidEndDecelerating is the one you want. Its UIScrollViewDelegates optional method:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

Tells the delegate that the scroll view has ended decelerating the scrolling movement.

UIScrollViewDelegate documentation

3
  • Er, I gave the same answer. :)
    – S.P
    Commented Jun 14, 2009 at 17:46
  • Doh. Somewhat cryptically named but its exactly what I was looking for. Should have read the documentation better. Its been a long day... Commented Jun 14, 2009 at 18:07
  • This one liner solved my problem. Quick fix for me! Thank you. Commented Dec 16, 2015 at 2:07
9
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    scrollingFinished(scrollView: scrollView)
}

func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    if decelerate {
        //didEndDecelerating will be called for sure
        return
    }
    scrollingFinished(scrollView: scrollView)        
}

func scrollingFinished(scrollView: UIScrollView) {
   // Your code
}
1
  • best solution, thx
    – Alex Ab
    Commented Sep 18, 2024 at 11:10
8

I only just found this question, which is pretty much the same I asked: How to know exactly when a UIScrollView's scrolling has stopped?

Though didEndDecelerating works when scrolling, panning with stationary release does not register.

I eventually found a solution. didEndDragging has a parameter WillDecelerate, which is false in the stationary release situation.

By checking for !decelerate in DidEndDragging, combined with didEndDecelerating, you get both situations that are the end of scrolling.

8

If you're into Rx, you can extend UIScrollView like this:

import RxSwift
import RxCocoa

extension Reactive where Base: UIScrollView {
    public var didEndScrolling: ControlEvent<Void> {
        let source = Observable
            .merge([base.rx.didEndDragging.map { !$0 },
                    base.rx.didEndDecelerating.mapTo(true)])
            .filter { $0 }
            .mapTo(())
        return ControlEvent(events: source)
    }
}

which will allow you to just do like this:

scrollView.rx.didEndScrolling.subscribe(onNext: {
    // Do what needs to be done here
})

This will take into account both dragging and deceleration.

0
4

If somebody needs, here's Ashley Smart answer in Swift

func scrollViewDidScroll(_ scrollView: UIScrollView) {
        NSObject.cancelPreviousPerformRequests(withTarget: self)
        perform(#selector(UIScrollViewDelegate.scrollViewDidEndScrollingAnimation), with: nil, afterDelay: 0.3)
    ...
}

func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
        NSObject.cancelPreviousPerformRequests(withTarget: self)
    ...
}
3

I've tried Ashley Smart's answer and it worked like a charm. Here's another idea, with using only scrollViewDidScroll

-(void)scrollViewDidScroll:(UIScrollView *)sender 
{   
    if(self.scrollView_Result.contentOffset.x == self.scrollView_Result.frame.size.width)       {
    // You have reached page 1
    }
}

I just had two pages so it worked for me. However, if you have more than one page, it could be problematic (you could check whether the current offset is a multiple of the width but then you wouldn't know if the user stopped at 2nd page or is on his way to 3rd or more)

3

Swift version of accepted answer:

func scrollViewDidScroll(scrollView: UIScrollView) {
     // example code
}
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        // example code
}
func scrollViewDidEndZooming(scrollView: UIScrollView, withView view: UIView!, atScale scale: CGFloat) {
      // example code
}
3

Just developed solution to detect when scrolling finished app-wide: https://gist.github.com/k06a/731654e3168277fb1fd0e64abc7d899e

It is based on idea of tracking changes of runloop modes. And perform blocks at least after 0.2 seconds after scrolling.

This is core idea for tracking runloop modes changes iOS10+:

- (void)tick {
    [[NSRunLoop mainRunLoop] performInModes:@[ UITrackingRunLoopMode ] block:^{
        [self tock];
    }];
}

- (void)tock {
    self.runLoopModeWasUITrackingAgain = YES;
    [[NSRunLoop mainRunLoop] performInModes:@[ NSDefaultRunLoopMode ] block:^{
        [self tick];
    }];
}

And solution for low deployment targets like iOS2+:

- (void)tick {
    [[NSRunLoop mainRunLoop] performSelector:@selector(tock) target:self argument:nil order:0 modes:@[ UITrackingRunLoopMode ]];
}

- (void)tock {
    self.runLoopModeWasUITrackingAgain = YES;
    [[NSRunLoop mainRunLoop] performSelector:@selector(tick) target:self argument:nil order:0 modes:@[ NSDefaultRunLoopMode ]];
}
2
  • @IsaacCarolWeisberg you could delay heavy operations until the time when smooth scrolling finished to keep it smooth.
    – k06a
    Commented Jun 5, 2020 at 20:40
  • heavy operations, you say... the ones that I am executing in the global concurrent dispatch queues, probably Commented Jun 6, 2020 at 14:59
2

To recap (and for newbies). It's not that painful. Just add the protocol, then add the functions you need for detection.

In the view (class) that contains the UIScrolView, add the protocol, then added any the functions from here to your view (class).

// --------------------------------
// In the "h" file:
// --------------------------------
@interface myViewClass : UIViewController  <UIScrollViewDelegate> // <-- Adding the protocol here

// Scroll view
@property (nonatomic, retain) UIScrollView *myScrollView;
@property (nonatomic, assign) BOOL isScrolling;

// Protocol functions
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView;


// --------------------------------
// In the "m" file:
// --------------------------------
@implementation BlockerViewController

- (void)viewDidLoad {
    CGRect scrollRect = self.view.frame; // Same size as this view
    self.myScrollView = [[UIScrollView alloc] initWithFrame:scrollRect];
    self.myScrollView.delegate = self;
    self.myScrollView.contentSize = CGSizeMake(scrollRect.size.width, scrollRect.size.height);
    self.myScrollView.contentInset = UIEdgeInsetsMake(0.0,22.0,0.0,22.0);
    // Allow dragging button to display outside the boundaries
    self.myScrollView.clipsToBounds = NO;
    // Prevent buttons from activating scroller:
    self.myScrollView.canCancelContentTouches = NO;
    self.myScrollView.delaysContentTouches = NO;
    [self.myScrollView setBackgroundColor:[UIColor darkGrayColor]];
    [self.view addSubview:self.myScrollView];

    // Add stuff to scrollview
    UIImage *myImage = [UIImage imageNamed:@"foo.png"];
    [self.myScrollView addSubview:myImage];
}

// Protocol functions
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    NSLog(@"start drag");
    _isScrolling = YES;
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    NSLog(@"end decel");
    _isScrolling = NO;
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    NSLog(@"end dragging");
    if (!decelerate) {
       _isScrolling = NO;
    }
}

// All of the available functions are here:
// https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIScrollViewDelegate_Protocol/Reference/UIScrollViewDelegate.html
2

I needed a callback that fires when all zoom and scroll gestures have ended and all animations like deceleration and zoom bounces have finished.

UIScrollView has a few flags that we can check, like isTracking and isDecelerating. We also have the various scrollViewDidEnd... delegate callbacks. However, the updates of these flags and the callbacks happen in an inconsistent order and it looks super difficult to cover all edge cases.

What I'm doing now is that I defer the check of the scroll view's flags to the next run loop. This ensures that all flags have been updated.

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
  dispatchScrollViewInteractionUpdate()
}

func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
  dispatchScrollViewInteractionUpdate()
}

func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
  dispatchScrollViewInteractionUpdate()
}

func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
  dispatchScrollViewInteractionUpdate()
}

private func dispatchScrollViewInteractionUpdate() {
  DispatchQueue.main.async {
    self.updateScrollViewInteraction()
  }
}

private func updateScrollViewInteraction() {    
  if !scrollView.isZooming
  && !scrollView.isDragging
  && !scrollView.isDecelerating
  && !scrollView.isZoomBouncing
  && !scrollView.isTracking {
    print("All animations and interactions have ended.")
  }
}
1

I had a case of tapping and dragging actions and I found out that the dragging was calling scrollViewDidEndDecelerating

And the change offset manually with code ([_scrollView setContentOffset:contentOffset animated:YES];) was calling scrollViewDidEndScrollingAnimation.

//This delegate method is called when the dragging scrolling happens, but no when the     tapping
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    //do whatever you want to happen when the scroll is done
}

//This delegate method is called when the tapping scrolling happens, but no when the  dragging
-(void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
{
     //do whatever you want to happen when the scroll is done
}
1

There is a method of UIScrollViewDelegate which can be used to detect (or better to say 'predict') when scrolling has really finished:

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

of UIScrollViewDelegate which can be used to detect (or better to say 'predict') when scrolling has really finished.

In my case I used it with horizontal scrolling as following (in Swift 3):

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    perform(#selector(self.actionOnFinishedScrolling), with: nil, afterDelay: Double(velocity.x))
}
func actionOnFinishedScrolling() {
    print("scrolling is finished")
    // do what you need
}
1

An alternative would be to use scrollViewWillEndDragging:withVelocity:targetContentOffset which is called whenever the user lifts the finger and contains the target content offset where the scroll will stop. Using this content offset in scrollViewDidScroll: correctly identifies when the scroll view has stopped scrolling.

private var targetY: CGFloat?
public func scrollViewWillEndDragging(_ scrollView: UIScrollView,
                                      withVelocity velocity: CGPoint,
                                      targetContentOffset: UnsafeMutablePointer<CGPoint>) {
       targetY = targetContentOffset.pointee.y
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
    if (scrollView.contentOffset.y == targetY) {
        print("finished scrolling")
    }
1

On some earlier iOS versions(like iOS 9, 10), scrollViewDidEndDecelerating won't be triggered if the scrollView is suddenly stopped by touching.

But in the current version (iOS 13), scrollViewDidEndDecelerating will be triggered for sure (As far as I know).

So, if your App targeted earlier versions as well, you might need a workaround like the one mentioned by Ashley Smart, or you can the following one.


    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        if !scrollView.isTracking, !scrollView.isDragging, !scrollView.isDecelerating { // 1
            scrollViewDidEndScrolling(scrollView)
        }
    }

    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        if !decelerate, scrollView.isTracking, !scrollView.isDragging, !scrollView.isDecelerating { // 2
            scrollViewDidEndScrolling(scrollView)
        }
    }

    func scrollViewDidEndScrolling(_ scrollView: UIScrollView) {
        // Do something here
    }

Explanation

UIScrollView will be stoped in three ways:
- quickly scrolled and stopped by itself
- quickly scrolled and stopped by finger touch (like Emergency brake)
- slowly scrolled and stopped

The first one can be detected by scrollViewDidEndDecelerating and other similar methods while the other two can't.

Luckily, UIScrollView has three statuses we can use to identify them, which is used in the two lines commented by "//1" and "//2".

1
1

I went through all the permutations and the best code I found was this. The interesting part is in 'scrollViewWillBeginDragging' and detecting if velocity is zero. This can occur when the user's finger is put on screen during an animating scroll to put the brakes on the scroll. For some reason, doing this does not trigger any of the other delegate 'stopping' API.

public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {        
    let velocity = scrollView.panGestureRecognizer.velocity(in: scrollView.superview)
    if (velocity.equalTo(CGPoint.zero)){
        scrollViewDidEndScrollingAnimation(scrollView)
    }
}


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

public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    scrollViewDidEndScrollingAnimation(scrollView)
}

public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
    // do whatever when scrolling stops
}
0

UIScrollview has a delegate method

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

Add the below lines of code in the delegate method

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
CGSize scrollview_content=scrollView.contentSize;
CGPoint scrollview_offset=scrollView.contentOffset;
CGFloat size=scrollview_content.width;
CGFloat x=scrollview_offset.x;
if ((size-self.view.frame.size.width)==x) {
    //You have reached last page
}
}
0

Using Ashley Smart logic and is being converted into Swift 4.0 and above.

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
         NSObject.cancelPreviousPerformRequests(withTarget: self)
        perform(#selector(UIScrollViewDelegate.scrollViewDidEndScrollingAnimation(_:)), with: scrollView, afterDelay: 0.3)
    }

    func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
        NSObject.cancelPreviousPerformRequests(withTarget: self)
    }

The logic above solve issues such as when user is scrolling off the tableview. Without the logic, when you scroll off the tableview, didEnd will be called but it will not execute anything. Currently using it in year 2020.

0

RXSwift version

commentsTableView.rx.didEndScrollingAnimation
    .bind(
        onNext: { _ in
            debugPrint(":DEBUG:", "didEndScrollingAnimation")
        }
    )
    .disposed(by: disposeBag)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.