152

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?

TheNeil
  • 1,985
  • 2
  • 17
  • 36
Michael Gaylord
  • 6,912
  • 8
  • 44
  • 47
  • See also, if you want to detect finished scrolling after scrolling programatically: http://stackoverflow.com/questions/2358046/is-it-possible-to-be-notified-when-a-uitableview-finishes-scrolling – Trevor Jul 07 '11 at 02:53
  • For `SwiftUI` - See https://stackoverflow.com/questions/65062590/swiftui-detect-when-scrollview-has-finished-scrolling – Mofawaw Nov 29 '20 at 18:04

19 Answers19

178

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];
...
}
mmackh
  • 3,470
  • 3
  • 33
  • 50
Ashley Smart
  • 2,041
  • 2
  • 13
  • 7
163
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    [self stoppedScrolling];
}

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

- (void)stoppedScrolling {
    // ...
}
Iulian Onofrei
  • 7,489
  • 8
  • 59
  • 96
Suvesh Pratapa
  • 8,036
  • 3
  • 35
  • 28
  • 14
    It looks like that only works if it is decelerating though. If you pan slowly, then release, it doesn't call didEndDecelerating. – Sean Clark Hess Nov 01 '10 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 Jun 14 '11 at 08:45
  • Works perfectly and the explanation makes sense – Steven Elliott Dec 18 '12 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. – Aurelien Porte Oct 15 '13 at 10:32
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.

Aurelien Porte
  • 2,583
  • 25
  • 31
  • 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 – Shadow Of Aug 05 '19 at 16:19
20

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
}
Wayne
  • 56,476
  • 13
  • 125
  • 118
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

texmex5
  • 4,274
  • 1
  • 24
  • 28
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.

Community
  • 1
  • 1
Aberrant
  • 3,283
  • 1
  • 24
  • 37
5

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)
    ...
}
Xernox
  • 1,587
  • 1
  • 19
  • 32
5
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
}
malhal
  • 17,500
  • 6
  • 94
  • 112
Umair
  • 956
  • 9
  • 11
5

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.

Zappel
  • 1,256
  • 1
  • 19
  • 34
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
}
zzzz
  • 58
  • 7
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 ]];
}
k06a
  • 14,368
  • 10
  • 57
  • 97
  • @IsaacCarolWeisberg you could delay heavy operations until the time when smooth scrolling finished to keep it smooth. – k06a Jun 05 '20 at 20:40
  • heavy operations, you say... the ones that I am executing in the global concurrent dispatch queues, probably – Isaaс Weisberg Jun 06 '20 at 14:59
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)

Ege Akpinar
  • 3,141
  • 20
  • 28
1

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
bob
  • 6,069
  • 1
  • 39
  • 35
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
}
Adriana
  • 191
  • 1
  • 8
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")
    }
Dog
  • 466
  • 8
  • 24
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".

JsW
  • 1,301
  • 2
  • 15
  • 30
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
}
}
Rahul K Rajan
  • 748
  • 10
  • 19
0

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
}
lexpenz
  • 51
  • 1
  • 3
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.

Kelvin Tan
  • 157
  • 1
  • 13