Comment détecter quand un UIScrollView a fini de défiler


145

UIScrollViewDelegate a deux méthodes de délégués scrollViewDidScroll:et scrollViewDidEndScrollingAnimation:mais aucun de ceux - ci vous dire quand le défilement est terminée. scrollViewDidScrollvous avertit seulement que la vue de défilement n'a pas défilé et non qu'elle a fini de défiler.

L'autre méthode scrollViewDidEndScrollingAnimationne semble se déclencher que si vous déplacez la vue de défilement par programme et non si l'utilisateur fait défiler.

Est-ce que quelqu'un connaît un système pour détecter quand une vue de défilement a terminé le défilement?


Voir aussi, si vous voulez détecter le défilement fini après le défilement par programme: stackoverflow.com/questions/2358046/...
Trevor

Réponses:


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

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

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

13
Il semble que cela ne fonctionne que s'il ralentit. Si vous effectuez un panoramique lentement, puis relâchez, cela n'appelle pas didEndDecelerating.
Sean Clark Hess

4
Bien que ce soit l'API officielle. En fait, cela ne fonctionne pas toujours comme prévu. @Ashley Smart a donné une solution plus pratique.
Di Wu

Fonctionne parfaitement et l'explication a du sens
Steven Elliott

Cela ne fonctionne que pour le défilement dû à une interaction de glissement. Si votre défilement est dû à autre chose (comme l'ouverture du clavier ou la fermeture du clavier), il semble que vous deviez détecter l'événement avec un hack, et scrollViewDidEndScrollingAnimation n'est pas non plus utile.
Aurelien Porte

176

le 320 implémentations sont tellement meilleures - voici un patch pour obtenir un début / une fin cohérents du 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];
...
}

La seule réponse utile chez SO pour mon problème de scroll. Merci!
Di Wu

4
devrait être [self performSelector: @selector (scrollViewDidEndScrollingAnimation :) withObject: sender afterDelay: 0.3]
Brainware

1
En 2015, c'est toujours la meilleure solution.
lukas

2
Honnêtement, je ne peux pas croire que je suis en février 2016, iOS 9.3 et c'est toujours la meilleure solution à ce problème. Merci,
j'ai

1
Tu m'as sauvé. Meilleure solution.
Baran Emre

21

Je pense que scrollViewDidEndDecelerating est celui que vous voulez. Sa méthode facultative UIScrollViewDelegates:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

Indique au délégué que la vue de défilement a cessé de ralentir le mouvement de défilement.

Documentation UIScrollViewDelegate


Euh, j'ai donné la même réponse. :)
Suvesh Pratapa

Doh. Un peu cryptiquement nommé mais c'est exactement ce que je cherchais. J'aurais dû mieux lire la documentation. Ça a été une longue journée ...
Michael Gaylord

Cette seule doublure a résolu mon problème. Solution rapide pour moi! Je vous remercie.
Dody Rachmat Wicaksono

20

Pour tous les parchemins liés aux interactions de glissement, cela suffira:

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

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

Maintenant, si votre défilement est dû à un setContentOffset / scrollRectVisible programmatique (avec animated= YES ou vous savez évidemment quand le défilement est terminé):

 - (void)scrollViewDidEndScrollingAnimation {
     _isScrolling = NO;
}

Si votre défilement est dû à autre chose (comme l'ouverture du clavier ou la fermeture du clavier), il semble que vous deviez détecter l'événement avec un hack car ce scrollViewDidEndScrollingAnimationn'est pas utile non plus.

Le cas d'une vue par défilement PAGINÉE :

Parce que, je suppose, Apple applique une courbe d'accélération, scrollViewDidEndDeceleratingest appelé pour chaque traînée, il n'est donc pas nécessaire de l'utiliser scrollViewDidEndDraggingdans ce cas.


Votre cas de vue de défilement paginé a un problème: si vous relâchez le défilement exactement à la position de fin de page (quand aucun défilement n'est nécessaire), scrollViewDidEndDeceleratingne sera pas appelé
Shadow Of

19

Cela a été décrit dans certaines des autres réponses, mais voici (en code) comment combiner scrollViewDidEndDeceleratinget scrollViewDidEndDragging:willDecelerateeffectuer certaines opérations lorsque le défilement est terminé:

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

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

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

7

Je viens de trouver cette question, qui est à peu près la même que celle que j'ai posée: Comment savoir exactement quand le défilement d'un UIScrollView s'est arrêté?

Bien que didEndDecelerating fonctionne lors du défilement, le panoramique avec relâchement stationnaire n'est pas enregistré.

J'ai finalement trouvé une solution. didEndDragging a un paramètre WillDecelerate, qui est faux dans la situation de libération stationnaire.

En recherchant! Decelerate dans DidEndDragging, combiné avec didEndDecelerating, vous obtenez les deux situations qui sont la fin du défilement.


5

Si vous êtes dans Rx, vous pouvez étendre UIScrollView comme ceci:

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

ce qui vous permettra de faire comme ceci:

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

Cela prendra en compte à la fois la traînée et la décélération.


Ceci est exactement ce que je cherchais. Merci!
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

J'ai essayé la réponse d'Ashley Smart et cela a fonctionné comme un charme. Voici une autre idée, en utilisant uniquement scrollViewDidScroll

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

Je n'avais que deux pages, donc ça a marché pour moi. Cependant, si vous avez plus d'une page, cela peut être problématique (vous pouvez vérifier si le décalage actuel est un multiple de la largeur, mais vous ne saurez pas si l'utilisateur s'est arrêté à la 2ème page ou est en route vers la 3ème ou plus)


3

Version rapide de la réponse acceptée:

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

Si quelqu'un a besoin, voici la réponse Ashley Smart dans 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

Solution juste développée pour détecter lorsque le défilement est terminé à l'échelle de l'application: https://gist.github.com/k06a/731654e3168277fb1fd0e64abc7d899e

Il est basé sur l'idée de suivre les changements des modes de boucle. Et effectuez des blocs au moins après 0,2 seconde après le défilement.

C'est l'idée de base pour le suivi des changements de modes de runloop iOS10 +:

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

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

Et solution pour les cibles de faible déploiement comme 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 ]];
}

Détection de défilement à l'échelle de l'application Lol - comme dans, si l'un des UIScrollVIew de l'application défile actuellement ... semble un peu inutile ...
Isaac Carol Weisberg

@IsaacCarolWeisberg vous pouvez retarder les opérations lourdes jusqu'au moment où le défilement en douceur est terminé pour le garder fluide.
k06a

opérations lourdes, dites-vous ... celles que j'exécute dans les files d'attente de répartition simultanées mondiales, probablement
Isaac Carol Weisberg

1

Pour récapituler (et pour les débutants). Ce n'est pas si douloureux. Ajoutez simplement le protocole, puis ajoutez les fonctions dont vous avez besoin pour la détection.

Dans la vue (classe) qui contient UIScrolView, ajoutez le protocole, puis ajoutez toutes les fonctions d'ici à votre vue (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

J'ai eu un cas de tapotement et de glissement d'actions et j'ai découvert que le glissement appelait scrollViewDidEndDecelerating

Et le décalage de modification manuellement avec le code ([_scrollView setContentOffset: contentOffset animated: YES];) appelait 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

Une alternative serait d'utiliser scrollViewWillEndDragging:withVelocity:targetContentOffsetqui est appelé chaque fois que l'utilisateur lève le doigt et contient le décalage du contenu cible où le défilement s'arrêtera. L'utilisation de ce décalage de contenu scrollViewDidScroll:identifie correctement le moment où la vue de défilement a cessé de défiler.

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 a une méthode déléguée

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

Ajoutez les lignes de code ci-dessous dans la méthode déléguée

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

Il existe une méthode permettant de UIScrollViewDelegatedétecter (ou mieux de dire `` prédire '') quand le défilement est vraiment terminé:

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

de UIScrollViewDelegate qui peut être utilisé pour détecter (ou mieux dire « prédire ») lors du défilement a vraiment terminé.

Dans mon cas, je l'ai utilisé avec le défilement horizontal comme suit (dans 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

Utilisation de la logique Ashley Smart et est en cours de conversion en Swift 4.0 et supérieur.

    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 logique ci-dessus résout des problèmes tels que lorsque l'utilisateur fait défiler la vue de table. Sans la logique, lorsque vous faites défiler la tableview, didEndsera appelée mais elle n'exécutera rien. L'utilise actuellement en 2020.


0

Sur certaines versions antérieures d'iOS (comme iOS 9, 10), scrollViewDidEndDecelerating ne sera pas déclenché si le scrollView est soudainement arrêté en touchant.

Mais dans la version actuelle (iOS 13), scrollViewDidEndDeceleratingsera déclenché à coup sûr (pour autant que je sache).

Donc, si votre application ciblait également des versions antérieures, vous pourriez avoir besoin d'une solution de contournement comme celle mentionnée par Ashley Smart, ou vous pouvez la suivante.


    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
    }

Explication

UIScrollView sera arrêté de trois manières:
- défilement rapide et arrêté par lui
- même - défilement rapide et arrêté par le toucher du doigt (comme le frein d'urgence)
- défilement lent et arrêté

Le premier peut être détecté par scrollViewDidEndDecelerating et d'autres méthodes similaires tandis que les deux autres ne le peuvent pas.

Heureusement, UIScrollViewa trois statuts que nous pouvons utiliser pour les identifier, qui est utilisé dans les deux lignes commentées par "// 1" et "// 2".

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.