How to detect when a UIScrollView has finished scrolling

IosIphoneUiscrollview

Ios Problem Overview


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?

Ios Solutions


Solution 1 - Ios

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];
...
}

Solution 2 - Ios

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

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

- (void)stoppedScrolling {
    // ...
}

Solution 3 - Ios

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
}

Solution 4 - Ios

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.

Solution 5 - Ios

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

Solution 6 - Ios

I only just found this question, which is pretty much the same I asked: https://stackoverflow.com/questions/7675497/how-to-know-exactly-when-a-uiscrollviews-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.

Solution 7 - Ios

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
}

Solution 8 - Ios

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)
    ...
}

Solution 9 - Ios

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.

Solution 10 - Ios

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)

Solution 11 - Ios

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
}

Solution 12 - Ios

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 ]];
}

Solution 13 - Ios

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

Solution 14 - Ios

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
}

Solution 15 - Ios

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
}

Solution 16 - Ios

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")
    }

Solution 17 - Ios

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".

Solution 18 - Ios

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
}
}

Solution 19 - Ios

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.

Solution 20 - Ios

RXSwift version

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMichael GaylordView Question on Stackoverflow
Solution 1 - IosAshley SmartView Answer on Stackoverflow
Solution 2 - IosSuPraView Answer on Stackoverflow
Solution 3 - IosWayneView Answer on Stackoverflow
Solution 4 - IosAurelien PorteView Answer on Stackoverflow
Solution 5 - Iostexmex5View Answer on Stackoverflow
Solution 6 - IosAberrantView Answer on Stackoverflow
Solution 7 - IosUmairView Answer on Stackoverflow
Solution 8 - IosXernoxView Answer on Stackoverflow
Solution 9 - IosZappelView Answer on Stackoverflow
Solution 10 - IosEge AkpinarView Answer on Stackoverflow
Solution 11 - IoszzzzView Answer on Stackoverflow
Solution 12 - Iosk06aView Answer on Stackoverflow
Solution 13 - IosbobView Answer on Stackoverflow
Solution 14 - IosAdrianaView Answer on Stackoverflow
Solution 15 - IoslexpenzView Answer on Stackoverflow
Solution 16 - IosDogView Answer on Stackoverflow
Solution 17 - IosJsWView Answer on Stackoverflow
Solution 18 - IosRahul K RajanView Answer on Stackoverflow
Solution 19 - IosKelvin TanView Answer on Stackoverflow
Solution 20 - IosОлег Александрович ПустошкинView Answer on Stackoverflow