Cara mendeteksi ketika UIScrollView telah selesai bergulir

145

UIScrollViewDelegate telah mendapatkan dua metode pendelegasian scrollViewDidScroll:dan scrollViewDidEndScrollingAnimation:tetapi tidak satupun dari ini yang memberi tahu Anda ketika gulir telah selesai. scrollViewDidScrollhanya memberi tahu Anda bahwa tampilan gulir tidak gulir yang belum selesai bergulir.

Metode lain scrollViewDidEndScrollingAnimationsepertinya hanya menyala jika Anda secara terprogram memindahkan tampilan gulir bukan jika pengguna menggulir.

Adakah yang tahu skema untuk mendeteksi ketika tampilan gulir telah selesai menggulir?

Michael Gaylord
sumber
Lihat juga, jika Anda ingin mendeteksi pengguliran yang selesai setelah pengguliran secara programatis: stackoverflow.com/questions/2358046/…
Trevor

Jawaban:

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

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

- (void)stoppedScrolling {
    // ...
}
Suvesh Pratapa
sumber
13
Sepertinya itu hanya berfungsi jika itu melambat sekalipun. Jika Anda menggeser perlahan, lalu lepaskan, itu tidak memanggil didEndDecelerating.
Sean Clark Hess
4
Padahal itu API resmi. Sebenarnya tidak selalu berhasil seperti yang kita harapkan. @Ashley Smart memberikan solusi yang lebih praktis.
Di Wu
Bekerja dengan sempurna dan penjelasannya masuk akal
Steven Elliott
Ini hanya berfungsi untuk gulir yang disebabkan oleh interaksi yang terseret. Jika gulir Anda disebabkan oleh sesuatu yang lain (seperti pembukaan keyboard atau penutupan keyboard), sepertinya Anda harus mendeteksi acara tersebut dengan peretasan, dan scrollViewDidEndScrollingAnimation juga tidak berguna.
Aurelien Porte
176

The 320 implementasi jauh lebih baik - di sini adalah patch untuk mendapatkan konsisten mulai / ujung gulungan itu.

-(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
sumber
Satu-satunya jawaban yang berguna di SO untuk masalah gulir saya. Terima kasih!
Di Wu
4
harus [self performSelector: @selector (scrollViewDidEndScrollingAnimation :) withObject: sender afterDelay: 0.3]
Brainware
1
Pada 2015, ini masih merupakan solusi terbaik.
lukas
2
Jujur saya tidak percaya saya di Februari 2016, iOS 9.3 dan ini masih merupakan solusi terbaik untuk masalah ini. Terima kasih, bekerja seperti pesona
gmogames
1
Kamu menyelamatkanku. Solusi terbaik.
Baran Emre
21

Saya pikir scrollViewDidEndDecelerating adalah yang Anda inginkan. Metode opsional UIScrollViewDelegates:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

Memberitahu delegasi bahwa tampilan gulir telah berakhir memperlambat gerakan gulir.

Dokumentasi UIScrollViewDelegate

texmex5
sumber
Eh, saya memberikan jawaban yang sama. :)
Suvesh Pratapa
Doh. Dinamai agak samar tapi persis apa yang saya cari. Seharusnya membaca dokumentasi lebih baik. Sudah hari yang panjang ...
Michael Gaylord
Liner yang satu ini menyelesaikan masalah saya. Perbaikan cepat untuk saya! Terima kasih.
Dody Rachmat Wicaksono
20

Untuk semua gulungan yang terkait dengan interaksi seret, ini sudah cukup:

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

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

Sekarang, jika gulir Anda disebabkan oleh setContentOffset / scrollRectVisible terprogram (dengan animated= YA atau Anda jelas tahu kapan gulir berakhir):

 - (void)scrollViewDidEndScrollingAnimation {
     _isScrolling = NO;
}

Jika gulir Anda disebabkan oleh sesuatu yang lain (seperti pembukaan keyboard atau penutupan keyboard), sepertinya Anda harus mendeteksi acara tersebut dengan peretasan karena scrollViewDidEndScrollingAnimationjuga tidak berguna.

Kasus tampilan gulir PAGINASI :

Karena, saya kira, Apple menerapkan kurva akselerasi, scrollViewDidEndDeceleratingdipanggil untuk setiap hambatan sehingga tidak perlu digunakan scrollViewDidEndDraggingdalam kasus ini.

Aurelien Porte
sumber
Kasing gulir tampilan paginasi Anda memiliki satu masalah: jika Anda melepaskan gulir tepat pada posisi ujung halaman (bila tidak diperlukan gulir), scrollViewDidEndDeceleratingtidak akan dipanggil
Shadow Of
19

Ini telah dijelaskan dalam beberapa jawaban lain, tetapi inilah (dalam kode) cara menggabungkan scrollViewDidEndDeceleratingdan scrollViewDidEndDragging:willDeceleratemelakukan beberapa operasi ketika pengguliran telah selesai:

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

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

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

Saya baru saja menemukan pertanyaan ini, yang hampir sama dengan yang saya tanyakan: Bagaimana cara mengetahui dengan tepat kapan pengguliran UIScrollView telah berhenti?

Meskipun didEndDecelerating berfungsi saat menggulir, panning dengan rilis stasioner tidak mendaftar.

Saya akhirnya menemukan solusi. didEndDragging memiliki parameter WillDecelerate, yang salah dalam situasi rilis stasioner.

Dengan memeriksa! Decelerate di DidEndDragging, dikombinasikan dengan didEndDecelerating, Anda mendapatkan kedua situasi yang merupakan akhir dari pengguliran.

Tidak sopan
sumber
5

Jika Anda menyukai Rx, Anda dapat memperluas UIScrollView seperti ini:

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

yang akan memungkinkan Anda untuk melakukan seperti ini:

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

Ini akan mempertimbangkan seret dan perlambatan.

Zappel
sumber
Ini persis apa yang saya cari. Terima kasih!
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
sumber
3

Saya sudah mencoba jawaban Ashley Smart dan itu berfungsi seperti pesona. Ini ide lain, dengan hanya menggunakan scrollViewDidScroll

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

Saya hanya punya dua halaman sehingga itu berhasil untuk saya. Namun, jika Anda memiliki lebih dari satu halaman, itu bisa bermasalah (Anda bisa memeriksa apakah offset saat ini merupakan kelipatan dari lebar tetapi kemudian Anda tidak akan tahu apakah pengguna berhenti di halaman ke-2 atau sedang menuju ke halaman ke-3 atau lebih)

Ege Akpinar
sumber
3

Versi cepat dari jawaban yang diterima:

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
sumber
3

Jika seseorang membutuhkan, inilah jawaban Ashley Smart di 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
sumber
2

Baru saja mengembangkan solusi untuk mendeteksi ketika menggulir selesai di seluruh aplikasi: https://gist.github.com/k06a/731654e3168277fb1fd0e64abc7d899e

Ini didasarkan pada ide pelacakan perubahan mode runloop. Dan melakukan blok setidaknya setelah 0,2 detik setelah bergulir.

Ini adalah ide inti untuk melacak perubahan mode runloop iOS10 +:

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

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

Dan solusi untuk target penyebaran rendah seperti 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
sumber
Lol deteksi gulir di seluruh aplikasi - seperti pada, jika ada UIScrollVIew aplikasi saat ini sedang bergulir ... tampaknya agak tidak berguna ...
Isaac Carol Weisberg
@IsaacCarolWeisberg Anda dapat menunda pengoperasian yang berat sampai waktu penggulungan yang mulus selesai untuk membuatnya tetap lancar.
k06a
operasi berat, Anda mengatakan ... orang-orang yang saya jalankan dalam antrian pengiriman global bersamaan, mungkin
Isaac Carol Weisberg
1

Untuk rekap (dan untuk pemula). Itu tidak menyakitkan. Cukup tambahkan protokol, lalu tambahkan fungsi yang Anda butuhkan untuk deteksi.

Dalam tampilan (kelas) yang berisi UIScrolView, tambahkan protokol, lalu tambahkan fungsi apa pun dari sini ke tampilan (kelas) Anda.

// --------------------------------
// 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
bob
sumber
1

Saya memiliki kasus tindakan mengetuk dan menyeret dan saya menemukan bahwa menyeret memanggil scrollViewDidEndDecelerating

Dan perubahan diimbangi secara manual dengan kode ([_scrollView setContentOffset: contentOffset animated: YES];) memanggil 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
sumber
1

Alternatifnya adalah menggunakan scrollViewWillEndDragging:withVelocity:targetContentOffsetyang dipanggil setiap kali pengguna mengangkat jari dan berisi offset konten target di mana gulir akan berhenti. Menggunakan konten ini diimbangi scrollViewDidScroll:dengan benar mengidentifikasi ketika tampilan gulir telah berhenti menggulir.

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")
    }
Anjing
sumber
0

UIScrollview memiliki metode pendelegasian

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

Tambahkan baris kode di bawah ini dalam metode delegasi

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

Ada metode UIScrollViewDelegateyang dapat digunakan untuk mendeteksi (atau lebih baik mengatakan 'prediksi') ketika pengguliran telah benar-benar selesai:

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

dari UIScrollViewDelegateyang dapat digunakan untuk mendeteksi (atau lebih baik untuk mengatakan 'memprediksi') ketika bergulir telah benar-benar selesai.

Dalam kasus saya, saya menggunakannya dengan pengguliran horizontal sebagai berikut (dalam 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
sumber
0

Menggunakan logika Ashley Smart dan sedang dikonversi ke Swift 4.0 dan lebih tinggi.

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

Logika di atas menyelesaikan masalah seperti ketika pengguna menggulir tampilan tabel. Tanpa logika, ketika Anda menggulir tampilan tabel, didEndakan dipanggil tetapi tidak akan menjalankan apa pun. Saat ini menggunakannya pada tahun 2020.

Kelvin Tan
sumber
0

Pada beberapa versi iOS sebelumnya (seperti iOS 9, 10), scrollViewDidEndDeceleratingtidak akan dipicu jika scrollView tiba-tiba dihentikan dengan menyentuh.

Tetapi dalam versi saat ini (iOS 13), scrollViewDidEndDeceleratingakan dipicu pasti (Sejauh yang saya tahu).

Jadi, jika Aplikasi Anda menargetkan versi sebelumnya juga, Anda mungkin memerlukan solusi seperti yang disebutkan oleh Ashley Smart, atau Anda dapat yang berikutnya.


    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
    }

Penjelasan

UIScrollView akan dihentikan dengan tiga cara:
- digulir dengan cepat dan dihentikan dengan sendirinya
- digulir dan dihentikan dengan cepat dengan sentuhan jari (seperti rem Darurat)
- perlahan digulir dan dihentikan

Yang pertama dapat dideteksi oleh scrollViewDidEndDeceleratingdan metode serupa lainnya sementara dua lainnya tidak.

Untungnya, UIScrollViewmemiliki tiga status yang dapat kita gunakan untuk mengidentifikasi mereka, yang digunakan dalam dua baris yang dikomentari oleh "// 1" dan "// 2".

JsW
sumber