Como detectar quando um UIScrollView termina a rolagem


145

UIScrollViewDelegate tem dois métodos de delegado scrollViewDidScroll:e scrollViewDidEndScrollingAnimation:, mas nenhum destes dizer quando rolagem foi concluída. scrollViewDidScrollnotifica apenas que a exibição de rolagem rolou e não que terminou a rolagem.

O outro método scrollViewDidEndScrollingAnimationsó parece acionar se você mover programaticamente a exibição de rolagem e não se o usuário rolar.

Alguém sabe de esquema para detectar quando uma exibição de rolagem concluiu a rolagem?


Veja também, se você quiser detectar rolagem acabado após a rolagem programaticamente: stackoverflow.com/questions/2358046/...
— Trevor

Respostas:


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

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

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

13
Parece que só funciona se estiver desacelerando. Se você mover lentamente, solte, não chamará didEndDecelerating.
— Sean Clark Hess

4
Embora seja a API oficial. Na verdade, nem sempre funciona como esperamos. @Ashley Smart deu uma solução mais prática.
— Di Wu

Funciona perfeitamente ea explicação faz sentido
— Steven Elliott

Isso funciona apenas para rolagem devido à interação de arrastar. Se o seu deslocamento for devido a outra coisa (como abertura ou fechamento do teclado), parece que você precisará detectar o evento com um hack e o scrollViewDidEndScrollingAnimation também não será útil.
— Aurelien Porte

176

As 320 implementações são muito melhores - aqui está um patch para obter início / fim consistentes do pergaminho.

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

A única resposta útil no SO para o meu problema de rolagem. Obrigado!
— Di Wu

4
deve ser [auto performSelector: @selector (scrollViewDidEndScrollingAnimation :) withObject: sender afterDelay: 0.3]
— Brainware

1
Em 2015, essa ainda é a melhor solução.
— Lukas

2
Sinceramente, não acredito que estou em fevereiro de 2016, iOS 9.3 e essa ainda é a melhor solução para esse problema. Obrigado, funcionou como um encanto
— gmogames

1
Você me salvou. Melhor solução.
— Baran Emre

21

Acho scrollViewDidEndDecelerating é o que você deseja. Seu método opcional UIScrollViewDelegates:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

Informa ao delegado que a exibição de rolagem terminou a desaceleração do movimento de rolagem.

Documentação do UIScrollViewDelegate


Eu dei a mesma resposta. :)
— Suvesh Pratapa

Doh. Um pouco enigmaticamente nomeado, mas é exatamente o que eu estava procurando. Deveria ter lido melhor a documentação. Tem sido um longo dia ...
— Michael Gaylord

Este forro resolveu meu problema. Solução rápida para mim! Obrigado.
— Dody Rachmat Wicaksono

20

Para todos os pergaminhos relacionados às interações de arrastar, isso será suficiente:

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

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

Agora, se o seu scroll for devido a um setContentOffset / scrollRectVisible programático (com animated= YES ou você obviamente sabe quando o scroll termina):

 - (void)scrollViewDidEndScrollingAnimation {
     _isScrolling = NO;
}

Se o seu deslocamento for devido a outra coisa (como abertura ou fechamento do teclado), parece que você terá que detectar o evento com um hack, porque scrollViewDidEndScrollingAnimationtambém não é útil.

O caso de uma exibição de rolagem PAGINATED :

Porque, eu acho, a Apple aplica uma curva de aceleração, scrollViewDidEndDeceleratingé chamada para cada arrastar, então não há necessidade de usar scrollViewDidEndDraggingneste caso.


O seu caso de exibição de rolagem paginada tem um problema: se você liberar a rolagem exatamente na posição final da página (quando nenhuma rolagem for necessária), scrollViewDidEndDeceleratingnão será chamado #
— Shadow Of

19

Isso foi descrito em algumas das outras respostas, mas aqui está (no código) como combinar scrollViewDidEndDeceleratinge scrollViewDidEndDragging:willDecelerateexecutar alguma operação quando a rolagem terminar:

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

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

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

7

Acabei de encontrar esta pergunta, que é praticamente a mesma que perguntei: Como saber exatamente quando a rolagem de um UIScrollView parou?

Embora didEndDecelerating funcione ao rolar, o movimento panorâmico com liberação estacionária não é registrado.

Acabei encontrando uma solução. didEndDragging possui um parâmetro WillDecelerate, que é falso na situação de liberação estacionária.

Ao verificar! Desacelerar em DidEndDragging, combinado com didEndDecelerating, você obtém as duas situações que são o fim da rolagem.


5

Se você gosta de Rx, pode estender o UIScrollView assim:

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

o que permitirá que você faça assim:

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

Isso levará em consideração o arraste e a desaceleração.


Era exatamente isso que eu estava procurando. Obrigado!
— Nomnom 08/08/19

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
}

3

Eu tentei a resposta de Ashley Smart e funcionou como um encanto. Aqui está outra idéia: usar apenas scrollViewDidScroll

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

Eu só tinha duas páginas, então funcionou para mim. No entanto, se você tiver mais de uma página, isso poderá ser problemático (você poderá verificar se o deslocamento atual é múltiplo da largura, mas não saberá se o usuário parou na 2ª página ou se está na terceira ou na terceira página. Mais)


3

Versão rápida da resposta aceita:

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

Se alguém precisar, aqui está a resposta Ashley Smart em 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)
    ...
}

2

Solução recém-desenvolvida para detectar quando a rolagem terminou em todo o aplicativo: https://gist.github.com/k06a/731654e3168277fb1fd0e64abc7d899e

Baseia-se na ideia de rastrear alterações nos modos runloop. E execute blocos pelo menos após 0,2 segundos após a rolagem.

Esta é a ideia principal para acompanhar os modos de execução em execução iOS10 +:

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

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

E solução para destinos de baixa implantação como o 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 ]];
}

Lol detecção de rolagem em todo o aplicativo - como em, se algum dos UIScrollVIew do aplicativo está rolando atualmente ... parece meio inútil ...
— Isaac Carol Weisberg

@IsaacCarolWeisberg, você pode adiar operações pesadas até o momento em que a rolagem suave termine para mantê-la suave.
— k06a

operações pesadas, você diz ... as que eu estou executando nas filas globais simultâneas de despacho, provavelmente
— Isaac Carol Weisberg

1

Para recapitular (e para iniciantes). Não é tão doloroso. Basta adicionar o protocolo e, em seguida, adicionar as funções necessárias para a detecção.

Na visualização (classe) que contém o UIScrolView, adicione o protocolo e, em seguida, adicione quaisquer funções daqui à sua visualização (classe).

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

1

Tive um caso de ações de tocar e arrastar e descobri que o arrasto chamava scrollViewDidEndDecelerating

E a alteração deslocada manualmente com o código ([_scrollView setContentOffset: contentOffset animated: YES];) estava chamando 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

Uma alternativa seria usar a scrollViewWillEndDragging:withVelocity:targetContentOffsetchamada sempre que o usuário levantar o dedo e contiver o deslocamento do conteúdo de destino onde a rolagem será interrompida. O uso desse deslocamento de conteúdo scrollViewDidScroll:identifica corretamente quando a exibição de rolagem parou de rolar.

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

0

O UIScrollview possui um método delegado

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

Adicione as linhas de código abaixo no 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
}
}

0

Existe um método UIScrollViewDelegateque pode ser usado para detectar (ou melhor, dizer 'prever') quando a rolagem realmente terminou:

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

dos UIScrollViewDelegatequais podem ser usados ​​para detectar (ou melhor, dizer 'prever') quando a rolagem realmente terminou.

No meu caso, usei-o com rolagem horizontal da seguinte forma (no 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
}

0

Usando a lógica Ashley Smart e está sendo convertido no Swift 4.0 e 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)
    }

A lógica acima resolve problemas como quando o usuário está rolando para fora da tableview. Sem a lógica, quando você rolar a tela, didEndserá chamado, mas não executará nada. Atualmente usando-o no ano 2020.


0

Em algumas versões anteriores do iOS (como iOS 9, 10), scrollViewDidEndDeceleratingnão será acionado se o scrollView for subitamente interrompido ao tocar.

Mas na versão atual (iOS 13), scrollViewDidEndDeceleratingserá acionado com certeza (tanto quanto eu sei).

Portanto, se o seu aplicativo também segmentou versões anteriores, você pode precisar de uma solução alternativa, como a mencionada por Ashley Smart, ou a seguinte.


    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
    }

Explicação

O UIScrollView será parado de três maneiras:
- rolou e parou
rapidamente sozinho - rolou e parou rapidamente com o toque do dedo (como freio de emergência)
- rolou e parou lentamente

O primeiro pode ser detectado por scrollViewDidEndDeceleratinge outros métodos similares, enquanto os outros dois não.

Felizmente, UIScrollViewpossui três status que podemos usar para identificá-los, que é usado nas duas linhas comentadas por "// 1" e "// 2".

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.