Cómo detectar cuándo un UIScrollView ha terminado de desplazarse

145

UIScrollViewDelegate tiene dos métodos de delegado scrollViewDidScroll:y scrollViewDidEndScrollingAnimation:ninguno de estos le indica cuándo se ha completado el desplazamiento. scrollViewDidScrollsolo le notifica que la vista de desplazamiento se desplazó, no que ha terminado de desplazarse.

El otro método scrollViewDidEndScrollingAnimationsolo parece activarse si mueve la vista de desplazamiento mediante programación, no si el usuario se desplaza.

¿Alguien sabe de esquema para detectar cuando una vista de desplazamiento ha completado el desplazamiento?

Michael Gaylord
fuente
Consulte también, si desea detectar el desplazamiento finalizado después de desplazarse programáticamente: stackoverflow.com/questions/2358046/…
Trevor

Respuestas:

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

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

- (void)stoppedScrolling {
    // ...
}
Suvesh Pratapa
fuente
13
Sin embargo, parece que solo funciona si se está desacelerando. Si se desplaza lentamente y luego se suelta, no se llama didEndDecelerating.
Sean Clark Hess el
44
Aunque es la API oficial. En realidad, no siempre funciona como esperamos. @Ashley Smart dio una solución más práctica.
Di Wu
Funciona perfectamente y la explicación tiene sentido
Steven Elliott
Esto funciona solo para el desplazamiento que se debe a la interacción de arrastre. Si su desplazamiento se debe a algo más (como abrir o cerrar el teclado), parece que tendrá que detectar el evento con un truco, y scrollViewDidEndScrollingAnimation tampoco es útil.
Aurelien Porte
176

Las 320 implementaciones son mucho mejores: aquí hay un parche para obtener un inicio / final consistente del desplazamiento.

-(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];
...
}
Ashley Smart
fuente
La única respuesta útil en SO para mi problema de desplazamiento. ¡Gracias!
Di Wu
44
debería ser [self performSelector: @selector (scrollViewDidEndScrollingAnimation :) withObject: sender afterDelay: 0.3]
Brainware
1
En 2015, esta sigue siendo la mejor solución.
lukas
2
Sinceramente, no puedo creer que esté en febrero de 2016, iOS 9.3 y esta sigue siendo la mejor solución para este problema. Gracias, funcionó como un encanto
gmogames
1
Me salvaste. Mejor solución.
Baran Emre
21

Creo que scrollViewDidEndDecelerating es el que quieres. Su método opcional UIScrollViewDelegates:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

Le dice al delegado que la vista de desplazamiento ha finalizado la desaceleración del movimiento de desplazamiento.

Documentación UIScrollViewDelegate

texmex5
fuente
Er, di la misma respuesta. :)
Suvesh Pratapa
Doh Algo crípticamente llamado pero es exactamente lo que estaba buscando. Debería haber leído mejor la documentación. Ha sido un día largo ...
Michael Gaylord
Este trazador de líneas resolvió mi problema. Solución rápida para mí! Gracias.
Dody Rachmat Wicaksono
20

Para todos los pergaminos relacionados con las interacciones de arrastre, esto será suficiente:

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

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

Ahora, si su desplazamiento se debe a un setContentOffset / scrollRectVisible programático (con animated= YES o obviamente sabe cuándo finaliza el desplazamiento):

 - (void)scrollViewDidEndScrollingAnimation {
     _isScrolling = NO;
}

Si su desplazamiento se debe a algo más (como abrir o cerrar el teclado), parece que tendrá que detectar el evento con un truco porque scrollViewDidEndScrollingAnimationtampoco es útil.

El caso de una vista de desplazamiento PAGINADA :

Porque, supongo, Apple aplica una curva de aceleración, scrollViewDidEndDeceleratingrecibe una llamada para cada arrastre, por lo que no hay necesidad de usar scrollViewDidEndDraggingen este caso.

Aurelien Porte
fuente
Su caso de vista de desplazamiento paginado tiene un problema: si suelta el desplazamiento exactamente en la posición final de la página (cuando no se necesita desplazamiento), scrollViewDidEndDeceleratingno se llamará
Shadow Of
19

Esto se ha descrito en algunas de las otras respuestas, pero aquí está (en código) cómo combinar scrollViewDidEndDeceleratingy scrollViewDidEndDragging:willDeceleraterealizar alguna operación cuando finalice el desplazamiento:

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

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

- (void)stoppedScrolling
{
    // done, do whatever
}
Wayne
fuente
7

Acabo de encontrar esta pregunta, que es más o menos lo mismo que pregunté: ¿Cómo saber exactamente cuándo se ha detenido el desplazamiento de UIScrollView?

Aunque didEndDecelerating funciona al desplazarse, el desplazamiento panorámico con liberación estacionaria no se registra.

Finalmente encontré una solución. didEndDragging tiene un parámetro WillDecelerate, que es falso en la situación de liberación estacionaria.

Al verificar! Desacelerar en DidEndDragging, combinado con didEndDecelerating, obtienes ambas situaciones que son el final del desplazamiento.

Aberrante
fuente
5

Si te gusta Rx, puedes extender UIScrollView así:

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

lo que te permitirá hacer lo siguiente:

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

Esto tendrá en cuenta tanto el arrastre como la desaceleración.

Zappel
fuente
Esto es exactamente lo que estaba buscando. ¡Gracias!
nomnom
4
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    scrollingFinished(scrollView: scrollView)
}

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

func scrollingFinished(scrollView: UIScrollView) {
   // Your code
}
Umair
fuente
3

He intentado la respuesta de Ashley Smart y funcionó de maravilla. Aquí hay otra idea, con usar solo scrollViewDidScroll

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

Solo tenía dos páginas, así que funcionó para mí. Sin embargo, si tiene más de una página, podría ser problemático (podría verificar si el desplazamiento actual es un múltiplo del ancho, pero entonces no sabría si el usuario se detuvo en la segunda página o está en camino a la tercera o más)

Ege Akpinar
fuente
3

Versión rápida de la respuesta aceptada:

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
fuente
3

Si alguien lo necesita, aquí está la respuesta de Ashley Smart en 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
fuente
2

Solución recién desarrollada para detectar cuando el desplazamiento finaliza en toda la aplicación: https://gist.github.com/k06a/731654e3168277fb1fd0e64abc7d899e

Se basa en la idea de seguir los cambios de los modos runloop. Y realice bloques al menos después de 0.2 segundos después del desplazamiento.

Esta es la idea central para el seguimiento de los cambios en los modos runloop iOS10 +:

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

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

Y solución para objetivos de baja implementación como 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
fuente
Detección de desplazamiento en toda la aplicación jajaja - como en, si alguno de los UIScrollVIew de la aplicación se está desplazando actualmente ... parece un poco inútil ...
Isaac Carol Weisberg
@IsaacCarolWeisberg puede retrasar las operaciones pesadas hasta el momento en que finalice el desplazamiento suave para mantenerlo suave.
k06a
operaciones pesadas, dices ... las que estoy ejecutando en las colas de despacho simultáneas globales, probablemente
Isaac Carol Weisberg
1

Para recapitular (y para novatos). No es tan doloroso. Simplemente agregue el protocolo, luego agregue las funciones que necesita para la detección.

En la vista (clase) que contiene UIScrolView, agregue el protocolo y luego agregue las funciones desde aquí a su vista (clase).

// --------------------------------
// 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
Beto
fuente
1

Tuve un caso de tocar y arrastrar acciones y descubrí que arrastrar llamaba scrollViewDidEndDecelerating

Y el cambio se compensó manualmente con el código ([_scrollView setContentOffset: contentOffset animated: YES];) estaba llamando a 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
fuente
1

Una alternativa sería usar scrollViewWillEndDragging:withVelocity:targetContentOffsetcual se llama cada vez que el usuario levanta el dedo y contiene el desplazamiento del contenido objetivo donde se detendrá el desplazamiento. El uso de este desplazamiento de contenido scrollViewDidScroll:identifica correctamente cuándo la vista de desplazamiento ha dejado de desplazarse.

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")
    }
Perro
fuente
0

UIScrollview tiene un método delegado

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

Agregue las siguientes líneas de código en el método delegado

- (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
fuente
0

Hay un método UIScrollViewDelegateque puede usarse para detectar (o mejor decir 'predecir') cuando el desplazamiento realmente ha terminado:

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

de los UIScrollViewDelegatecuales se pueden usar para detectar (o mejor decir 'predecir') cuando el desplazamiento realmente ha terminado.

En mi caso, lo usé con desplazamiento horizontal de la siguiente manera (en 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
fuente
0

Usa la lógica Ashley Smart y se está convirtiendo en Swift 4.0 y superior.

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

La lógica anterior resuelve problemas como cuando el usuario se desplaza fuera de la vista de tabla. Sin la lógica, cuando salga de la vista de tabla, didEndse llamará pero no ejecutará nada. Actualmente usándolo en el año 2020.

Kelvin Tan
fuente
0

En algunas versiones anteriores de iOS (como iOS 9, 10), scrollViewDidEndDeceleratingno se activará si el scrollView se detiene repentinamente al tocar.

Pero en la versión actual (iOS 13), scrollViewDidEndDeceleratingse activará con seguridad (que yo sepa).

Entonces, si su aplicación también apuntó a versiones anteriores, es posible que necesite una solución alternativa como la mencionada por Ashley Smart, o puede la siguiente.


    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
    }

Explicación

UIScrollView se detendrá de tres maneras:
- se desplaza y se detiene rápidamente por sí mismo
- se desplaza y se detiene rápidamente con el toque de un dedo (como el freno de emergencia)
- se desplaza y se detiene lentamente

El primero se puede detectar mediante scrollViewDidEndDeceleratingy otros métodos similares, mientras que los otros dos no.

Afortunadamente, UIScrollViewtiene tres estados que podemos usar para identificarlos, que se usa en las dos líneas comentadas por "// 1" y "// 2".

JsW
fuente