Come rilevare quando UIScrollView ha terminato lo scorrimento


145

UIScrollViewDelegate ha due metodi delegati scrollViewDidScroll:e scrollViewDidEndScrollingAnimation:nessuno dei due ti dice quando lo scorrimento è completato. scrollViewDidScrollti avvisa solo che la vista di scorrimento non ha fatto scorrere lo scorrimento.

L'altro metodo scrollViewDidEndScrollingAnimationsembra attivarsi solo se si sposta a livello di codice la vista di scorrimento, non se l'utente scorre.

Qualcuno conosce lo schema per rilevare quando una vista di scorrimento ha completato lo scorrimento?


Vedi anche, se vuoi rilevare lo scorrimento finito dopo lo scorrimento programmatico: stackoverflow.com/questions/2358046/…
Trevor

Risposte:


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

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

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

13
Sembra che funzioni solo se sta rallentando. Se esegui il pan lentamente, quindi rilascia, non chiama didEndDecelerating.
Sean Clark Hess,

4
Sebbene sia l'API ufficiale. In realtà non funziona sempre come ci aspettiamo. @Ashley Smart ha dato una soluzione più pratica.
Di Wu,

Funziona perfettamente e la spiegazione ha un senso
Steven Elliott il

Questo funziona solo per lo scorrimento dovuto al trascinamento dell'interazione. Se lo scorrimento è dovuto a qualcos'altro (come l'apertura della tastiera o la chiusura della tastiera), sembra che dovrai rilevare l'evento con un hack e scrollViewDidEndScrollingAnimation non è utile.
Aurelien Porte,

176

Le 320 implementazioni sono molto migliori: ecco una patch per ottenere inizio / fine coerenti dello 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];
...
}

L'unica risposta utile a SO per il mio problema di scorrimento. Grazie!
Di Wu,

4
dovrebbe essere [self performSelector: @selector (scrollViewDidEndScrollingAnimation :) withObject: mittente dopo Ritardo: 0,3]
Brainware,

1
Nel 2015, questa è ancora la soluzione migliore.
lukas,

2
Onestamente non riesco a credere di essere a febbraio 2016, iOS 9.3 e questa è ancora la migliore soluzione a questo problema. Grazie, ha funzionato a
meraviglia

1
Mi hai salvato. Soluzione migliore.
Baran Emre,

21

Penso che scrollViewDidEndDecelerating sia quello che desideri. Il suo metodo opzionale UIScrollViewDelegates:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

Indica al delegato che la vista di scorrimento ha terminato la decelerazione del movimento di scorrimento.

Documentazione UIScrollViewDelegate


Ehm, ho dato la stessa risposta. :)
Suvesh Pratapa,

Doh. Un po 'cripticamente chiamato ma è esattamente quello che stavo cercando. Avrei dovuto leggere meglio la documentazione. È stata una lunga giornata ...
Michael Gaylord,

Questa fodera ha risolto il mio problema. Soluzione rapida per me! Grazie.
Dody Rachmat Wicaksono il

20

Per tutti gli scroll correlati al trascinamento delle interazioni, questo sarà sufficiente:

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

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

Ora, se lo scorrimento è dovuto a un set programmaticoContentOffset / scrollRectVisible (con animated= SÌ o ovviamente sai quando lo scorrimento è terminato):

 - (void)scrollViewDidEndScrollingAnimation {
     _isScrolling = NO;
}

Se lo scorrimento è dovuto a qualcos'altro (come l'apertura della tastiera o la chiusura della tastiera), sembra che dovrai rilevare l'evento con un hack perché scrollViewDidEndScrollingAnimationnon è utile neanche.

Il caso di una vista di scorrimento PAGINATA :

Perché, suppongo, Apple applica una curva di accelerazione, scrollViewDidEndDeceleratingviene chiamata per ogni trascinamento, quindi non è necessario utilizzare scrollViewDidEndDraggingin questo caso.


Il caso della vista di scorrimento impaginata presenta un problema: se si rilascia lo scorrimento esattamente alla fine della pagina (quando non è necessario lo scorrimento), scrollViewDidEndDeceleratingnon verrà chiamato
Shadow Of

19

Questo è stato descritto in alcune delle altre risposte, ma ecco (in codice) come combinare scrollViewDidEndDeceleratinged scrollViewDidEndDragging:willDecelerateeseguire alcune operazioni al termine dello scorrimento:

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

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

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

7

Ho appena trovato questa domanda, che è praticamente la stessa che ho fatto: come sapere esattamente quando lo scorrimento di UIScrollView si è fermato?

Sebbene didEndDecelerating funzioni durante lo scorrimento, la panoramica con rilascio fisso non si registra.

Alla fine ho trovato una soluzione. didEndDragging ha un parametro WillDecelerate, che è falso nella situazione di rilascio stazionario.

Controllando! Decelerare in DidEndDragging, combinato con didEndDecelerating, si ottengono entrambe le situazioni che terminano lo scorrimento.


5

Se ti piace Rx, puoi estendere UIScrollView in questo modo:

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

che ti permetterà di fare così:

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

Ciò terrà conto sia del trascinamento che della decelerazione.


Questo e 'esattamente quello che stavo cercando. Grazie!
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
}

3

Ho provato la risposta di Ashley Smart e ha funzionato come un fascino. Ecco un'altra idea, usando solo scrollViewDidScroll

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

Ho appena avuto due pagine, quindi ha funzionato per me. Tuttavia, se hai più di una pagina, potrebbe essere problematico (potresti verificare se l'offset attuale è un multiplo della larghezza ma non sapresti se l'utente si è fermato alla seconda pagina o sta per arrivare alla terza o Di Più)


3

Versione rapida della risposta accettata:

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 qualcuno ha bisogno, ecco la risposta di Ashley Smart 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)
    ...
}

2

Soluzione appena sviluppata per rilevare quando lo scorrimento è finito a livello di app: https://gist.github.com/k06a/731654e3168277fb1fd0e64abc7d899e

Si basa sull'idea di tenere traccia delle modifiche delle modalità runloop. Ed esegui i blocchi almeno dopo 0,2 secondi dopo lo scorrimento.

Questa è l'idea principale per tenere traccia delle modifiche delle modalità runloop iOS10 +:

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

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

E soluzione per target di distribuzione bassi come 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 ]];
}

Rilevamento scorrimento a livello di app Lol - come in, se uno degli UIScrollVIew dell'app sta attualmente scorrendo ... sembra un po 'inutile ...
Isaac Carol Weisberg

@IsaacCarolWeisberg potresti ritardare le operazioni pesanti fino al termine dello scorrimento regolare per mantenerlo regolare.
k06a,

operazioni pesanti, dite ... quelle che sto eseguendo nelle code di spedizione simultanee globali, probabilmente
Isaac Carol Weisberg il

1

Ricapitolando (e per i neofiti). Non è così doloroso. Basta aggiungere il protocollo, quindi aggiungere le funzioni necessarie per il rilevamento.

Nella vista (classe) che contiene UIScrolView, aggiungi il protocollo, quindi aggiungi tutte le funzioni da qui alla tua vista (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

Ho avuto un caso di toccare e trascinare le azioni e ho scoperto che il trascinamento stava chiamando scrollViewDidEndDecelerating

E la modifica dell'offset manualmente con il codice ([_scrollView setContentOffset: contentOffset animato: YES];) chiamava 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

Un'alternativa sarebbe quella scrollViewWillEndDragging:withVelocity:targetContentOffsetche viene chiamata ogni volta che l'utente solleva il dito e contiene l'offset del contenuto di destinazione dove si fermerà lo scorrimento. L'uso di questo offset del contenuto in scrollViewDidScroll:identifica correttamente quando la vista di scorrimento ha smesso di scorrere.

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

UIScrollview ha un metodo delegato

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

Aggiungi le seguenti righe di codice nel metodo delegato

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

Esiste un metodo UIScrollViewDelegateche può essere utilizzato per rilevare (o meglio dire "predire") quando lo scorrimento è veramente terminato:

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

di UIScrollViewDelegatecui può essere utilizzato per rilevare (o meglio dire "prevedere") quando lo scorrimento è terminato.

Nel mio caso l'ho usato con scorrimento orizzontale come segue (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
}

0

Utilizzando la logica Ashley Smart e viene convertito in Swift 4.0 e versioni successive.

    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 logica sopra risolve problemi come quando l'utente sta scorrendo la vista tabella. Senza la logica, quando scorri dalla vista tabella, didEndverrà chiamato ma non eseguirà nulla. Attualmente in uso nel 2020.


0

In alcune versioni precedenti di iOS (come iOS 9, 10), scrollViewDidEndDeceleratingnon verrà attivato se scrollView si interrompe improvvisamente toccando.

Ma nella versione corrente (iOS 13), scrollViewDidEndDeceleratingverrà sicuramente attivato (per quanto ne so).

Quindi, se la tua app ha preso di mira anche le versioni precedenti, potresti aver bisogno di una soluzione alternativa come quella menzionata da Ashley Smart, oppure puoi utilizzare la seguente.


    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
    }

Spiegazione

UIScrollView verrà arrestato in tre modi:
- scorrimento rapido e arresto da solo
- scorrimento rapido e arresto mediante tocco delle dita (come il freno di emergenza)
- scorrimento lento e arresto

Il primo può essere rilevato da scrollViewDidEndDeceleratinge altri metodi simili mentre gli altri due non possono.

Fortunatamente, UIScrollViewha tre stati che possiamo usare per identificarli, che viene utilizzato nelle due righe commentate da "// 1" e "// 2".

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.