Как определить, когда UIScrollView завершил прокрутку

145

UIScrollViewDelegate имеет два метода делегата scrollViewDidScroll:и , scrollViewDidEndScrollingAnimation:но ни один из них не скажет вам , когда листание завершена. scrollViewDidScrollтолько уведомляет вас о том, что представление прокрутки прокручивается, но не завершило прокрутку.

Другой метод scrollViewDidEndScrollingAnimationсрабатывает только в том случае, если вы программно перемещаете представление прокрутки, а не при прокрутке пользователем.

Кто-нибудь знает схему обнаружения прокрутки после завершения просмотра?

Майкл Гейлорд
источник
Смотрите также, если вы хотите обнаружить законченную прокрутку после программной прокрутки: stackoverflow.com/questions/2358046/…
Тревор

Ответы:

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

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

- (void)stoppedScrolling {
    // ...
}
Сувеш Пратапа
источник
13
Похоже, что это работает, только если замедляется. Если вы перемещаетесь медленно, затем отпускаете, он не вызывает didEndDecelerating.
Шон Кларк Хесс
4
Хотя это официальный API. На самом деле это не всегда работает, как мы ожидаем. @Ashley Smart дал более практичное решение.
Ди У
Работает отлично, и объяснение имеет смысл
Стивен Эллиотт
Это работает только для прокрутки, вызванной перетаскиванием. Если ваша прокрутка вызвана чем-то другим (например, открытием клавиатуры или закрытием клавиатуры), кажется, что вам придется обнаруживать событие с помощью взлома, и scrollViewDidEndScrollingAnimation также бесполезен.
Aurelien Porte
176

Реализации 320 намного лучше - вот патч, чтобы получить последовательные начало / конец прокрутки.

-(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];
...
}
Эшли Смарт
источник
Единственный полезный ответ на SO для моей проблемы с прокруткой. Спасибо!
Ди Ву
4
должно быть [self executeSelector: @selector (scrollViewDidEndScrollingAnimation :) withObject: sender afterDelay: 0.3]
Brainware
1
В 2015 году это все еще лучшее решение.
Лукас
2
Я, честно говоря, не могу поверить, что я нахожусь в феврале 2016 года, iOS 9.3, и это все еще лучшее решение этой проблемы. Спасибо, работал как шарм
gmogames
1
Ты спас меня. Лучшее решение.
Баран Эмре
21

Я думаю, что scrollViewDidEndDecelerating - это то, что вам нужно. Его UIScrollViewDelegates необязательный метод:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

Сообщает делегату, что представление прокрутки прекратило замедление движения прокрутки.

UIScrollViewDelegate документация

texmex5
источник
Я дал тот же ответ. :)
Сувеш Пратапа
Doh. Как-то загадочно названо, но это именно то, что я искал. Надо было читать документацию лучше. Это был долгий день ...
Майкл Гейлорд
Этот лайнер решил мою проблему. Быстрое решение для меня! Спасибо.
Доди Рахмат Викаксоно
20

Для всех свитков, связанных с перетаскиванием взаимодействий, этого будет достаточно:

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

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

Теперь, если ваша прокрутка вызвана программным setContentOffset / scrollRectVisible (с animated= YES или вы, очевидно, знаете, когда прокрутка закончена):

 - (void)scrollViewDidEndScrollingAnimation {
     _isScrolling = NO;
}

Если ваша прокрутка вызвана чем-то другим (например, открытием клавиатуры или закрытием клавиатуры), похоже, что вам придется обнаруживать событие с помощью хака, потому что scrollViewDidEndScrollingAnimationэто также бесполезно.

Случай с прокруткой PAGINATED :

Потому что, я думаю, Apple применяет кривую ускорения, scrollViewDidEndDeceleratingвызывается для каждого перетаскивания, поэтому scrollViewDidEndDraggingв этом случае нет необходимости использовать .

Аурелиен Порт
источник
Ваш случай просмотра прокрутки с разбивкой по страницам имеет одну проблему: если вы отпустите прокрутку точно в конечной позиции страницы (когда прокрутка не нужна), scrollViewDidEndDeceleratingона не будет вызвана
Shadow Of
19

Это было описано в некоторых других ответах, но вот (в коде), как объединить scrollViewDidEndDeceleratingи scrollViewDidEndDragging:willDecelerateвыполнить некоторую операцию после завершения прокрутки:

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

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

- (void)stoppedScrolling
{
    // done, do whatever
}
Wayne
источник
7

Я только что нашел этот вопрос, который в значительной степени совпадает с тем, который я задал: Как точно узнать, когда прокрутка UIScrollView прекратилась?

Хотя didEndDecelerating работает при прокрутке, панорамирование со стационарным выпуском не регистрируется.

В конце концов я нашел решение. У didEndDragging есть параметр WillDecelerate, который является ложным в ситуации стационарного выпуска.

Проверяя на! Замедление в DidEndDragging, в сочетании с didEndDecelerating, вы получаете обе ситуации, которые являются концом прокрутки.

отклоняющийся
источник
5

Если вы в Rx, вы можете расширить UIScrollView следующим образом:

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

что позволит вам просто сделать так:

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

Это будет учитывать как перетаскивание и замедление.

Zappel
источник
Это именно то, что я искал. Спасибо!
Номном
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
источник
3

Я попробовал ответ Эшли Смарт, и он сработал как шарм. Вот еще одна идея, с использованием только scrollViewDidScroll

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

У меня просто было две страницы, так что у меня получилось. Однако, если у вас более одной страницы, это может быть проблематично (вы можете проверить, кратно ли текущее смещение ширине, но тогда вы не узнаете, остановился ли пользователь на 2-й странице или находится на пути к 3-й или Больше)

Эге Акпинар
источник
3

Быстрая версия принятого ответа:

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
источник
3

Если кто-то нуждается, вот ответ Эшли Смарт в 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
источник
2

Только что разработанное решение для обнаружения при прокрутке завершено для всего приложения: https://gist.github.com/k06a/731654e3168277fb1fd0e64abc7d899e

Он основан на идее отслеживания изменений режимов runloop. И выполнять блоки по крайней мере через 0,2 секунды после прокрутки.

Это основная идея для отслеживания изменений режимов runloop iOS10 +:

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

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

И решение для целей с низким уровнем развертывания, таких как 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
источник
Обнаружение прокрутки во всем приложении - как в случае, если какая-либо из UIScrollVIew приложения в данный момент прокручивает ... кажется бесполезной ...
Исаак Кэрол Вайсберг
@IsaacCarolWeisberg, вы можете отложить тяжелые операции до тех пор, пока не закончится плавная прокрутка, чтобы сохранить плавность.
k06a
тяжелые операции, вы говорите ... те, которые я выполняю в глобальных параллельных очередях отправки, вероятно
Исаак Кэрол Вайсберг
1

Подведем итоги (и для новичков). Это не так больно. Просто добавьте протокол, затем добавьте функции, необходимые для обнаружения.

В представлении (классе), которое содержит UIScrolView, добавьте протокол, а затем добавьте любые функции отсюда в ваше представление (класс).

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

У меня был случай касания и перетаскивания действий, и я обнаружил, что перетаскивание вызывало scrollViewDidEndDecelerating

И смещение изменения вручную с кодом ([_scrollView setContentOffset: contentOffset animated: YES];) вызывало 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
источник
1

Альтернативой может быть использование, scrollViewWillEndDragging:withVelocity:targetContentOffsetкоторое вызывается всякий раз, когда пользователь поднимает палец, и содержит смещение целевого содержимого, в котором прокрутка останавливается. Использование этого смещения содержимого scrollViewDidScroll:правильно определяет, когда представление прокрутки прекратило прокрутку.

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 имеет метод делегата

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

Добавьте следующие строки кода в методе делегата

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

Существует метод, с помощью UIScrollViewDelegateкоторого можно определить (или лучше сказать «предсказать»), когда прокрутка действительно завершена:

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

из UIScrollViewDelegateкоторых можно использовать для определения (или лучше сказать «предсказать»), когда прокрутка действительно завершена.

В моем случае я использовал его с горизонтальной прокруткой следующим образом (в 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
источник
0

Использует логику Эшли Смарт и конвертируется в Swift 4.0 и выше.

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

Приведенная выше логика решает проблемы, например, когда пользователь прокручивает таблицу. Без логики, когда вы прокручиваете таблицу, вызывается, didEndно она ничего не будет выполнять. В настоящее время использую его в 2020 году.

Кельвин Тан
источник
0

На некоторых более ранних версиях iOS (например, iOS 9, 10) scrollViewDidEndDeceleratingне будет запускаться, если scrollView внезапно останавливается при касании.

Но в текущей версии (iOS 13), scrollViewDidEndDeceleratingбудет срабатывать точно (насколько я знаю).

Поэтому, если ваше приложение нацелено и на более ранние версии, вам может понадобиться обходной путь, подобный тому, который упоминал Эшли Смарт, или следующий.


    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
    }

объяснение

UIScrollView будет остановлен тремя способами:
- быстро прокручивается и останавливается сам по себе
- быстро прокручивается и останавливается прикосновением пальца (например, аварийный тормоз)
- медленно прокручивается и останавливается

Первый может быть обнаружен scrollViewDidEndDeceleratingи другими подобными методами, а два других - нет.

К счастью, у нас UIScrollViewесть три состояния, которые мы можем использовать для их идентификации, что используется в двух строках с комментариями «// 1» и «// 2».

JSW
источник