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?

Michael Gaylord
fonte
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 {
    // ...
}
Suvesh Pratapa
fonte
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];
...
}
Ashley Smart
fonte
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

texmex5
fonte
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.

Aurelien Porte
fonte
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
}
Wayne
fonte
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.

Aberrante
fonte
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.

Zappel
fonte
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
}
Umair
fonte
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)

Ege Akpinar
fonte
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
}
zzzz
fonte
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)
    ...
}
Xernox
fonte
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 ]];
}
k06a
fonte
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
prumo
fonte
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
}
Adriana
fonte
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")
    }
Cão
fonte
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
}
}
Rahul K Rajan
fonte
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
}
lexpenz
fonte
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.

Kelvin Tan
fonte
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".

JsW
fonte