Jak wykryć zakończenie przewijania UIScrollView

145

UIScrollViewDelegate ma dwie metody delegata scrollViewDidScroll:a scrollViewDidEndScrollingAnimation:jednak żadna z nich nie powiedzieć podczas przewijania została zakończona. scrollViewDidScrollinformuje tylko o tym, że widok przewijania został przewinięty, a nie o zakończeniu przewijania.

Druga metoda scrollViewDidEndScrollingAnimationwydaje się uruchamiać tylko wtedy, gdy programowo przesuniesz widok przewijania, a nie, gdy użytkownik przewija.

Czy ktoś zna schemat wykrywania zakończenia przewijania widoku przewijania?

Michael Gaylord
źródło
Zobacz także, jeśli chcesz wykryć zakończone przewijanie po przewijaniu programowym: stackoverflow.com/questions/2358046/…
Trevor

Odpowiedzi:

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

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

- (void)stoppedScrolling {
    // ...
}
Suvesh Pratapa
źródło
13
Wygląda na to, że działa tylko wtedy, gdy zwalnia. Jeśli przesuniesz powoli, a następnie zwolnij, nie wywoła to didEndDecelerating.
Sean Clark Hess
4
Chociaż jest to oficjalne API. W rzeczywistości nie zawsze działa zgodnie z naszymi oczekiwaniami. @Ashley Smart dał bardziej praktyczne rozwiązanie.
Di Wu
Działa doskonale, a wyjaśnienie ma sens
Steven Elliott,
Działa to tylko w przypadku przewijania spowodowanego interakcją przeciągania. Jeśli przewijanie jest spowodowane czymś innym (np. Otwarciem lub zamknięciem klawiatury), wydaje się, że będziesz musiał wykryć zdarzenie za pomocą hackowania, a scrollViewDidEndScrollingAnimation również nie jest przydatny.
Aurelien Porte
176

Do 320 implementacje są o wiele lepiej - tutaj jest to poprawka, aby uzyskać spójne start / końce zwoju.

-(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
źródło
Jedyna przydatna odpowiedź w SO na mój problem z przewijaniem. Dzięki!
Di Wu,
4
powinno być [self performSelector: @selector (scrollViewDidEndScrollingAnimation :) withObject: sender afterDelay: 0.3]
Brainware
1
W 2015 roku jest to nadal najlepsze rozwiązanie.
lukas
2
Naprawdę nie mogę uwierzyć, że jestem w lutym 2016 r., IOS 9.3 i nadal jest to najlepsze rozwiązanie tego problemu. Dzięki, działało jak marzenie
gmogames
1
Uratowałeś mnie. Najlepsze rozwiązanie.
Baran Emre
21

Myślę, że scrollViewDidEndDecelerating to ten, którego chcesz. Jego opcjonalna metoda UIScrollViewDelegates:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

Informuje delegata, że ​​widok przewijania zakończył spowolnienie ruchu przewijania.

Dokumentacja UIScrollViewDelegate

texmex5
źródło
Eee, udzieliłem tej samej odpowiedzi. :)
Suvesh Pratapa
Doh. Nieco tajemniczo nazwany, ale dokładnie to, czego szukałem. Powinienem był lepiej przeczytać dokumentację. To był długi dzień ...
Michael Gaylord,
Ten jeden liner rozwiązał mój problem. Szybka naprawa dla mnie! Dziękuję Ci.
Dody Rachmat Wicaksono,
20

W przypadku wszystkich zwojów związanych z przeciąganiem interakcji będzie to wystarczające:

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

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

Teraz, jeśli przewijanie wynika z programowego setContentOffset / scrollRectVisible (z animated= YES lub oczywiście wiesz, kiedy przewijanie się zakończyło):

 - (void)scrollViewDidEndScrollingAnimation {
     _isScrolling = NO;
}

Jeśli przyczyną przewijania jest coś innego (np. Otwarcie lub zamknięcie klawiatury), wydaje się, że będziesz musiał wykryć zdarzenie za pomocą hackowania, ponieważ również scrollViewDidEndScrollingAnimationnie jest przydatne.

Przypadek widoku przewijania PAGINATED :

Ponieważ, jak sądzę, Apple stosuje krzywą przyspieszenia, jest scrollViewDidEndDeceleratingwywoływany przy każdym oporze, więc nie ma potrzeby używania scrollViewDidEndDraggingw tym przypadku.

Aurelien Porte
źródło
Twój przypadek widoku przewijania z podziałem na strony ma jeden problem: jeśli zwolnisz przewijanie dokładnie na końcu strony (gdy przewijanie nie jest potrzebne), scrollViewDidEndDeceleratingnie zostanie wywołane
Shadow Of
19

Zostało to opisane w niektórych innych odpowiedziach, ale oto (w kodzie), jak połączyć scrollViewDidEndDeceleratingi scrollViewDidEndDragging:willDeceleratewykonać jakąś operację po zakończeniu przewijania:

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

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

- (void)stoppedScrolling
{
    // done, do whatever
}
Wayne
źródło
7

Dopiero co znalazłem to pytanie, które jest prawie takie samo, jak zadałem: Skąd dokładnie wiedzieć, kiedy zatrzymało się przewijanie UIScrollView?

Chociaż didEndDecelerating działa podczas przewijania, panoramowanie ze zwolnieniem stacjonarnym nie jest rejestrowane.

W końcu znalazłem rozwiązanie. didEndDragging ma parametr WillDecelerate, który jest fałszywy w przypadku zwolnienia stacjonarnego.

Sprawdzając! Decelerate w DidEndDragging, w połączeniu z didEndDecelerating, otrzymujesz obie sytuacje, które kończą przewijanie.

Anormalny
źródło
5

Jeśli interesujesz się Rx, możesz rozszerzyć UIScrollView w następujący sposób:

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

co pozwoli ci zrobić tak po prostu:

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

Uwzględni to zarówno przeciąganie, jak i zwalnianie.

Zappel
źródło
To jest dokładnie to, czego szukałem. Dzięki!
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
źródło
3

Wypróbowałem odpowiedź Ashley Smart i zadziałała jak urok. Oto inny pomysł, z użyciem tylko scrollViewDidScroll

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

Miałem tylko dwie strony, więc zadziałało. Jeśli jednak masz więcej niż jedną stronę, może to być problematyczne (możesz sprawdzić, czy bieżące przesunięcie jest wielokrotnością szerokości, ale wtedy nie wiesz, czy użytkownik zatrzymał się na drugiej stronie, czy jest w drodze na trzecią lub więcej)

Ege Akpinar
źródło
3

Szybka wersja zaakceptowanej odpowiedzi:

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
źródło
3

Jeśli ktoś potrzebuje, oto odpowiedź Ashley Smart w języku 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
źródło
2

Właśnie opracowane rozwiązanie do wykrywania przewijania w całej aplikacji: https://gist.github.com/k06a/731654e3168277fb1fd0e64abc7d899e

Opiera się na idei śledzenia zmian trybów runloop. I wykonaj bloki co najmniej po 0,2 sekundy po przewinięciu.

Oto podstawowa idea śledzenia zmian trybów runloop iOS10 +:

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

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

I rozwiązanie dla małych celów wdrożeniowych, takich jak 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
źródło
Wykrywanie przewijania w całej aplikacji Lol - tak jak w przypadku, gdy któryś z UIScrollVIew aplikacji jest obecnie przewijany ... wydaje się trochę bezużyteczny ...
Isaac Carol Weisberg
@IsaacCarolWeisberg możesz opóźnić ciężkie operacje do czasu zakończenia płynnego przewijania, aby zachować płynność.
k06a
ciężkie operacje, mówisz ... te, które wykonuję w globalnych kolejkach wysyłek, prawdopodobnie
Isaac Carol Weisberg
1

Podsumowując (i dla początkujących). To nie jest takie bolesne. Po prostu dodaj protokół, a następnie dodaj funkcje potrzebne do wykrywania.

W widoku (klasie), który zawiera UIScrolView, dodaj protokół, a następnie dodaj wszystkie funkcje z tego miejsca do swojego widoku (klasy).

// --------------------------------
// 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
pion
źródło
1

Miałem przypadek stukania i przeciągania akcji i odkryłem, że przeciąganie wywoływało scrollViewDidEndDecelerating

Zmiana przesunięcia ręcznie za pomocą kodu ([_scrollView setContentOffset: contentOffset animated: YES];) wywoływał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
}
Adrianna
źródło
1

Alternatywą byłoby użycie, scrollViewWillEndDragging:withVelocity:targetContentOffsetktóre jest wywoływane za każdym razem, gdy użytkownik podniesie palec i zawiera docelowe przesunięcie zawartości, w którym przewijanie się zatrzyma. Użycie tego przesunięcia zawartości w scrollViewDidScroll:prawidłowo identyfikuje, kiedy widok przewijania przestał się przewijać.

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")
    }
Pies
źródło
0

UIScrollview ma metodę delegata

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

Dodaj poniższe wiersze kodu w metodzie delegata

- (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
źródło
0

Istnieje metoda, UIScrollViewDelegatektórej można użyć do wykrycia (lub lepiej powiedzieć „przewidywanie”), kiedy przewijanie naprawdę się zakończyło:

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

z UIScrollViewDelegatektórych mogą być stosowane do wykrywania (lub lepiej powiedzieć „przewidzieć”) podczas przewijania naprawdę skończony.

W moim przypadku użyłem go z przewijaniem poziomym w następujący sposób (w 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
źródło
0

Korzysta z logiki Ashley Smart i jest przekształcany w Swift 4.0 i nowsze.

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

Powyższa logika rozwiązuje problemy, takie jak przewijanie przez użytkownika widoku tabeli. Bez logiki, kiedy przewiniesz widok tabeli, didEndzostanie wywołany, ale nic nie wykona. Obecnie używa go w roku 2020.

Kelvin Tan
źródło
0

W niektórych wcześniejszych wersjach iOS (takich jak iOS 9, 10) scrollViewDidEndDeceleratingnie zostanie wyzwolony, jeśli scrollView zostanie nagle zatrzymany przez dotknięcie.

Ale w obecnej wersji (iOS 13) scrollViewDidEndDeceleratingna pewno zostanie uruchomiony (o ile wiem).

Jeśli więc Twoja aplikacja była skierowana również do wcześniejszych wersji, możesz potrzebować obejścia takiego jak to, o którym wspomina Ashley Smart, lub możesz użyć następującego.


    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
    }

Wyjaśnienie

UIScrollView zostanie zatrzymany na trzy sposoby:
- szybko przewijany i zatrzymywany samoczynnie
- szybko przewijany i zatrzymywany przez dotknięcie palcem (jak hamulec awaryjny)
- powoli przewijany i zatrzymywany

Pierwszą można wykryć scrollViewDidEndDeceleratingi innymi podobnymi metodami, podczas gdy dwie pozostałe nie.

Na szczęście UIScrollViewma trzy stany, których możemy użyć do ich identyfikacji, które są używane w dwóch wierszach opatrzonych komentarzem „// 1” i „// 2”.

JsW
źródło