Cara mendeteksi akhir pemuatan UITableView


141

Saya ingin mengubah ofset tabel ketika beban selesai dan offset itu tergantung pada jumlah sel yang dimuat pada tabel.

Apakah tetap di SDK untuk mengetahui kapan pemuatan uitableview selesai? Saya tidak melihat apa pun pada delegasi maupun pada protokol sumber data.

Saya tidak bisa menggunakan hitungan sumber data karena memuat sel yang terlihat saja.


coba kombinasi jumlah sumber data dan 'indexPathsForVisibleRows'
Swapnil Luktuke

1
Dibuka kembali berdasarkan info lebih lanjut dalam tanda: "Ini bukan duplikat. Ia menanyakan tentang memuat sel yang terlihat, bukan tentang penyelesaian data yang diminta. Lihat pembaruan untuk jawaban yang diterima"
Kev

Solusi ini berfungsi dengan baik untuk saya. Anda memeriksanya stackoverflow.com/questions/1483581/…
Khaled Annajar

Jawaban:


224

Tingkatkan ke jawaban @RichX: lastRowbisa keduanya [tableView numberOfRowsInSection: 0] - 1atau ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).row. Jadi kodenya adalah:

-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if([indexPath row] == ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).row){
        //end of loading
        //for example [activityIndicator stopAnimating];
    }
}

UPDATE: Baiklah, komentar @ htafoya benar. Jika Anda ingin kode ini mendeteksi akhir pemuatan semua data dari sumber, itu tidak akan terjadi, tetapi itu bukan pertanyaan awal. Kode ini untuk mendeteksi ketika semua sel yang seharusnya terlihat ditampilkan. willDisplayCell:digunakan di sini untuk UI lebih halus (sel tunggal biasanya menampilkan cepat setelah willDisplay:panggilan). Anda juga bisa mencobanya tableView:didEndDisplayingCell:.


Jauh lebih baik untuk mengetahui kapan semua sel memuat yang terlihat.
Eric

14
Namun ini akan dipanggil setiap kali pengguna menggulir untuk melihat lebih banyak sel.
htafoya

Masalahnya adalah ini tidak memperhitungkan tampilan footer. Mungkin tidak menjadi masalah bagi sebagian besar, tetapi jika itu Anda dapat menerapkan ide yang sama tetapi dalam viewForFooterInSectionmetode ini.
Kyle Clegg

1
@KyleClegg patric.schenke menyebutkan salah satu alasan dalam komentar atas jawaban Anda. Juga, bagaimana jika footer tidak terlihat di akhir pemuatan sel?
folex

27
tableView: didEndDisplayingCell: sebenarnya dipanggil saat menghapus sel dari tampilan, bukan ketika rendering dalam tampilan selesai sehingga tidak akan berfungsi. Bukan nama metode yang bagus. Harus membaca dokumen.
Andrew Raphael

34

Versi Swift 3 & 4 & 5:

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    if let lastVisibleIndexPath = tableView.indexPathsForVisibleRows?.last {
        if indexPath == lastVisibleIndexPath {
            // do here...
        }
    }
}

1
metode ini dipanggil saat pertama kali memuat data. Dalam kasus saya hanya ada 4 sel yang terlihat. Saya memuat 8 baris tetapi fungsi ini tetap dipanggil. Yang saya inginkan adalah memuat lebih banyak data ketika pengguna gulir ke bawah ke baris terakhir
Muhammad Nayab

Halo Muhammad Nayab, mungkin Anda bisa mengganti "if indexPath == lastVisibleIndexPath" dengan "if indexPath.row == yourDataSource.count"
Daniele Ceglia

1
@DanieleCeglia Terima kasih telah menunjukkan ini. jika indexPath == lastVisibleIndexPath && indexPath.row == yourDataSource.count - 1 ini akan bekerja untuk saya. Bersulang!!!
Yogesh Patel

28

Saya selalu menggunakan solusi yang sangat sederhana ini:

-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if([indexPath row] == lastRow){
        //end of loading
        //for example [activityIndicator stopAnimating];
    }
}

bagaimana cara mendeteksi baris terakhir? Itulah masalahnya bagi saya .. dapatkah Anda menjelaskan bagaimana Anda mendapatkan yang terakhir ke dalam kondisi jika.
Sameera Chathuranga

6
Baris terakhir adalah [tableView numberOfRowsInSection: 0] - 1. Anda harus mengganti 0dengan nilai yang dibutuhkan. Tapi bukan itu masalahnya. Masalahnya adalah bahwa UITableView memuat hanya terlihat. Namun ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).rowmenyelesaikan masalah.
folex

1
Jika kita mencari penyelesaian absolut bukankah sebaiknya menggunakan - (void) tableView: (UITableView *) tableView didEndDisplayingCell: (UITableViewCell *) sel untukRowAtIndexPath: (NSIndexPath *) indexPath?
morcutt

10

Inilah opsi lain yang sepertinya berhasil untuk saya. Dalam metode delegasi viewForFooter periksa apakah itu bagian terakhir dan tambahkan kode Anda di sana. Pendekatan ini datang ke pikiran setelah menyadari bahwa willDisplayCell tidak memperhitungkan footer jika Anda memilikinya.

- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section 
{
  // Perform some final layout updates
  if (section == ([tableView numberOfSections] - 1)) {
    [self tableViewWillFinishLoading:tableView];
  }

  // Return nil, or whatever view you were going to return for the footer
  return nil;
}

- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section
{
  // Return 0, or the height for your footer view
  return 0.0;
}

- (void)tableViewWillFinishLoading:(UITableView *)tableView
{
  NSLog(@"finished loading");
}

Saya menemukan pendekatan ini bekerja paling baik jika Anda mencari untuk menemukan pemuatan akhir untuk keseluruhan UITableView, dan bukan hanya sel yang terlihat. Tergantung pada kebutuhan Anda, Anda mungkin hanya menginginkan sel yang terlihat, dalam hal ini jawaban folex adalah rute yang baik.


Kembali nildari tableView:viewForFooterInSection:berantakan dengan tata letak saya di iOS 7 menggunakan Tata Letak Otomatis.
ma11hew28

Menarik. Bagaimana jika Anda mengaturnya ke bingkai dengan tinggi dan lebar 0? return CGRectMake(0,0,0,0);
Kyle Clegg

Saya mencoba itu dan bahkan mengatur self.tableView.sectionFooterHeight = 0. Either way, tampaknya untuk memasukkan tampilan bagian footer dengan ketinggian sekitar 10. Saya yakin saya bisa memperbaikinya dengan menambahkan batasan 0-tinggi pada tampilan yang saya kembalikan. Bagaimanapun, saya baik karena saya benar-benar ingin mengetahui cara memulai UITableView pada sel terakhir tetapi melihatnya terlebih dahulu.
ma11hew28

1
@MattDiPasquale jika Anda menerapkan viewForFooterInSection, Anda juga harus menerapkan heightForFooterInSection. Itu harus mengembalikan 0 untuk bagian dengan footer nil. Ini juga dalam dokumen resmi sekarang.
patric.schenke

viewForFooterInSection tidak dipanggil jika Anda menetapkan heightForFooterInSection ke 0
Oded Harth

10

Solusi Swift 2:

// willDisplay function
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
    let lastRowIndex = tableView.numberOfRowsInSection(0)
    if indexPath.row == lastRowIndex - 1 {
        fetchNewDataFromServer()
    }
}

// data fetcher function
func fetchNewDataFromServer() {
    if(!loading && !allDataFetched) {
        // call beginUpdates before multiple rows insert operation
        tableView.beginUpdates()
        // for loop
        // insertRowsAtIndexPaths
        tableView.endUpdates()
    }
}

9

Menggunakan API pribadi:

@objc func tableViewDidFinishReload(_ tableView: UITableView) {
    print(#function)
    cellsAreLoaded = true
}

Menggunakan API publik:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // cancel the perform request if there is another section
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(tableViewDidLoadRows:) object:tableView];

    // create a perform request to call the didLoadRows method on the next event loop.
    [self performSelector:@selector(tableViewDidLoadRows:) withObject:tableView afterDelay:0];

    return [self.myDataSource numberOfRowsInSection:section];
}

// called after the rows in the last section is loaded
-(void)tableViewDidLoadRows:(UITableView*)tableView{
    self.cellsAreLoaded = YES;
}

Sebuah desain yang mungkin lebih baik adalah dengan menambahkan sel-sel yang terlihat ke set, maka ketika Anda perlu memeriksa apakah tabel dimuat, Anda dapat melakukan perulangan for sekitar set ini, misalnya

var visibleCells = Set<UITableViewCell>()

override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    visibleCells.insert(cell)
}

override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    visibleCells.remove(cell)
}

// example property you want to show on a cell that you only want to update the cell after the table is loaded. cellForRow also calls configure too for the initial state.
var count = 5 {
    didSet {
        for cell in visibleCells {
            configureCell(cell)
        }
    }
}

Solusi yang jauh lebih baik daripada yang lain. Tidak memerlukan pemuatan tableView. Ini sangat sederhana dan viewDidLoadRows: tidak dipanggil setiap kali sel terakhir dimuat.
Matt Hudson

ini adalah solusi terbaik sejauh ini, karena solusi lain tidak memperhitungkan banyak bagian
justicepenny

Saya minta maaf atas komentar snarky, tapi bagaimana tepatnya 'sihir' ini? Menelepon ke metode lain dari delegasi. Saya tidak mengerti.
Lukas Petr

@LukasPetr lihat batal dan afterDelay. Itu memungkinkan panggilan metode dibatalkan untuk setiap bagian kecuali yang terakhir, yaitu ketika tabel telah selesai memuat.
Malhal

@malhal oh, oke. Ini sedikit lebih pintar dari yang saya kira pada pandangan pertama.
Lukas Petr

8

Untuk versi jawaban yang dipilih di Swift 3:

var isLoadingTableView = true

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    if tableData.count > 0 && isLoadingTableView {
        if let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows, let lastIndexPath = indexPathsForVisibleRows.last, lastIndexPath.row == indexPath.row {
            isLoadingTableView = false
            //do something after table is done loading
        }
    }
}

Saya membutuhkan variabel isLoadingTableView karena saya ingin memastikan tabel selesai memuat sebelum saya membuat pilihan sel default. Jika Anda tidak memasukkan ini maka setiap kali Anda menggulir tabel itu akan meminta kode Anda lagi.



3

Untuk mengetahui kapan tampilan tabel selesai memuat kontennya, pertama-tama kita harus memiliki pemahaman dasar tentang bagaimana tampilan ditampilkan di layar.

Dalam siklus hidup suatu aplikasi, ada 4 momen kunci:

  1. Aplikasi menerima acara (sentuh, timer, blok dikirim dll)
  2. Aplikasi ini menangani acara (memodifikasi kendala, memulai animasi, mengubah latar belakang dll)
  3. Aplikasi menghitung hierarki tampilan baru
  4. Aplikasi merender hierarki tampilan dan menampilkannya

Waktu 2 dan 3 benar-benar terpisah. MengapaUntuk alasan kinerja, kami tidak ingin melakukan semua perhitungan saat 3 setiap kali modifikasi dilakukan.

Jadi, saya pikir Anda menghadapi kasus seperti ini:

tableView.reloadData()
tableView.visibleCells.count // wrong count oO

Ada apa di sini?

Seperti tampilan apa pun, tampilan tabel memuat ulang kontennya dengan malas. Sebenarnya, jika Anda menelepon reloadDataberkali-kali tidak akan menimbulkan masalah kinerja. Tampilan tabel hanya menghitung ulang ukuran kontennya berdasarkan implementasi delegasinya dan menunggu saat 3 untuk memuat sel-selnya. Kali ini disebut tata letak pass.

Oke, bagaimana cara masuk ke tata letak lulus?

Selama pass tata letak, aplikasi menghitung semua bingkai hierarki tampilan. Untuk terlibat, Anda dapat mengganti metode khusus layoutSubviews, updateLayoutConstraintsdll dalam a UIViewdan metode yang setara dalam subkelas pengontrol tampilan.

Itulah yang dilakukan tampilan tabel. Ini menimpa layoutSubviewsdan berdasarkan pada implementasi delegasi Anda menambah atau menghapus sel. Itu panggilan cellForRowtepat sebelum menambahkan dan meletakkan sel baru, willDisplaytepat setelah. Jika Anda menelepon reloadDataatau baru saja menambahkan tampilan tabel ke hierarki, tampilan tabel menambahkan sebanyak mungkin sel untuk mengisi bingkai pada saat penting ini.

Baiklah, tapi sekarang, bagaimana cara mengetahui kapan tampilan tabel selesai memuat ulang isinya?

Kita sekarang dapat mengulangi pertanyaan ini: bagaimana cara mengetahui kapan tampilan tabel telah selesai menyusun subview?

Cara termudah adalah masuk ke tata letak tampilan tabel:

class MyTableView: UITableView {
    func layoutSubviews() {
        super.layoutSubviews()
        // the displayed cells are loaded
    }
}

Perhatikan bahwa metode ini dipanggil beberapa kali dalam siklus hidup tampilan tabel. Karena gulir dan perilaku dequeue dari tampilan tabel, sel-sel dimodifikasi, dihapus dan sering ditambahkan. Tapi itu berhasil, tepat setelah super.layoutSubviews()sel-sel dimuat. Solusi ini setara dengan menunggu willDisplayacara jalur indeks terakhir. Acara ini disebut selama pelaksanaanlayoutSubviews tampilan tabel untuk setiap sel yang ditambahkan.

Cara lain adalah dipanggil ketika aplikasi menyelesaikan pass tata letak.

Seperti yang dijelaskan dalam dokumentasi , Anda dapat menggunakan opsi dari UIView.animate(withDuration:completion):

tableView.reloadData()
UIView.animate(withDuration: 0) {
    // layout done
}

Solusi ini berfungsi tetapi layar akan menyegarkan sekali antara waktu tata letak selesai dan waktu blok dipanggil. Ini setara dengan DispatchMain.asyncsolusi tetapi ditentukan.

Atau, saya lebih suka memaksakan tata letak tampilan tabel

Ada metode khusus untuk memaksa tampilan apa pun untuk segera menghitung frame subview-nya layoutIfNeeded:

tableView.reloadData()
table.layoutIfNeeded()
// layout done

Namun hati-hati, hal itu akan menghapus pemuatan malas yang digunakan oleh sistem. Memanggil metode itu berulang kali dapat menciptakan masalah kinerja. Pastikan mereka tidak akan dipanggil sebelum bingkai tampilan tabel dihitung, jika tidak tampilan tabel akan dimuat lagi dan Anda tidak akan diberitahu.


Saya pikir tidak ada solusi yang sempurna. Kelas subkelas dapat menyebabkan trubles. Layout tata letak dimulai dari atas dan pergi ke bawah sehingga tidak mudah untuk mendapatkan pemberitahuan ketika semua tata letak dilakukan. Dan layoutIfNeeded()dapat membuat masalah kinerja dll. Tetapi mengetahui pilihan-pilihan itu, Anda harus dapat berpikir pada satu alternatif yang sesuai dengan kebutuhan Anda.


1

Inilah cara Anda melakukannya di Swift 3:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    if indexPath.row == 0 {
        // perform your logic here, for the first row in the table
    }

    // ....
}

1

di sini adalah bagaimana saya melakukannya di Swift 3

let threshold: CGFloat = 76.0 // threshold from bottom of tableView

internal func scrollViewDidScroll(_ scrollView: UIScrollView) {

    let contentOffset = scrollView.contentOffset.y
    let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height;

    if  (!isLoadingMore) &&  (maximumOffset - contentOffset <= threshold) {
        self.loadVideosList()
    }
}

1

Inilah yang akan saya lakukan.

  1. Di kelas dasar Anda (bisa berupa rootVC BaseVc dll),

    A. Tulis Protokol untuk mengirim panggilan balik "DidFinishReloading".

    @protocol ReloadComplition <NSObject>
    @required
    - (void)didEndReloading:(UITableView *)tableView;
    @end

    B. Tulis metode generik untuk memuat ulang tampilan tabel.

    -(void)reloadTableView:(UITableView *)tableView withOwner:(UIViewController *)aViewController;
  2. Dalam implementasi metode kelas dasar, panggil reloadData diikuti oleh delegateMethod dengan penundaan.

    -(void)reloadTableView:(UITableView *)tableView withOwner:(UIViewController *)aViewController{
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            [tableView reloadData];
            if(aViewController && [aViewController respondsToSelector:@selector(didEndReloading:)]){
                [aViewController performSelector:@selector(didEndReloading:) withObject:tableView afterDelay:0];
            }
        }];
    }
  3. Konfirmasikan ke protokol penyelesaian ulang di semua pengontrol tampilan tempat Anda membutuhkan panggilan balik.

    -(void)didEndReloading:(UITableView *)tableView{
        //do your stuff.
    }

Referensi: https://discussions.apple.com/thread/2598339?start=0&tstart=0


0

@folex jawabannya benar.

Tetapi itu akan gagal jika tableView memiliki lebih dari satu bagian ditampilkan pada satu waktu.

-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
   if([indexPath isEqual:((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject])]){
    //end of loading

 }
}

0

Di Swift Anda dapat melakukan sesuatu seperti ini. Kondisi berikut akan benar setiap kali Anda mencapai ujung tableView

func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
        if indexPath.row+1 == postArray.count {
            println("came to last row")
        }
}


0

Jika Anda memiliki beberapa bagian, berikut ini cara mendapatkan baris terakhir di bagian terakhir (Swift 3):

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    if let visibleRows = tableView.indexPathsForVisibleRows, let lastRow = visibleRows.last?.row, let lastSection = visibleRows.map({$0.section}).last {
        if indexPath.row == lastRow && indexPath.section == lastSection {
            // Finished loading visible rows

        }
    }
}

0

Secara tidak sengaja saya menemukan solusi ini:

tableView.tableFooterView = UIView()
tableViewHeight.constant = tableView.contentSize.height

Anda perlu mengatur footerView sebelum mendapatkan contentSize misalnya di viewDidLoad. Btw. mengatur footeView memungkinkan Anda menghapus pemisah "yang tidak digunakan"


-1

Apakah Anda mencari jumlah total item yang akan ditampilkan dalam tabel atau total item yang saat ini terlihat? Either way .. Saya percaya bahwa metode 'viewDidLoad' dijalankan setelah semua metode sumber data dipanggil. Namun, ini hanya akan berfungsi pada pemuatan pertama data (jika Anda menggunakan ViewController alokasi tunggal).


-1

Saya menyalin kode Andrew dan memperluasnya ke akun untuk kasus di mana Anda hanya memiliki 1 baris dalam tabel. Ini bekerja sejauh ini untuk saya!

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
// detect when all visible cells have been loaded and displayed
// NOTE: iOS7 workaround used - see: http://stackoverflow.com/questions/4163579/how-to-detect-the-end-of-loading-of-uitableview?lq=1
NSArray *visibleRows = [tableView indexPathsForVisibleRows];
NSIndexPath *lastVisibleCellIndexPath = [visibleRows lastObject];
BOOL isPreviousCallForPreviousCell = self.previousDisplayedIndexPath.row + 1 == lastVisibleCellIndexPath.row;
BOOL isLastCell = [indexPath isEqual:lastVisibleCellIndexPath];
BOOL isFinishedLoadingTableView = isLastCell && ([tableView numberOfRowsInSection:0] == 1 || isPreviousCallForPreviousCell);

self.previousDisplayedIndexPath = indexPath;

if (isFinishedLoadingTableView) {
    [self hideSpinner];
}
}

CATATAN: Saya hanya menggunakan 1 bagian dari kode Andrew, jadi ingatlah itu ..


Selamat datang di SO @ jpage4500. Saya telah mengedit jawaban Anda untuk menghapus hal-hal tentang poin rep rendah dan bump pertanyaan. Memperluas jawaban pengguna lain adalah jawaban yang benar-benar valid, sehingga Anda dapat mengurangi kekacauan dengan mengabaikannya. Bagus bahwa Anda memberi Andrew penghargaan, jadi itu tetap.
skrrgwasme

-3

Di iOS7.0x solusinya agak berbeda. Inilah yang saya pikirkan.

    - (void)tableView:(UITableView *)tableView 
      willDisplayCell:(UITableViewCell *)cell 
    forRowAtIndexPath:(NSIndexPath *)indexPath
{
    BOOL isFinishedLoadingTableView = [self isFinishedLoadingTableView:tableView  
                                                             indexPath:indexPath];
    if (isFinishedLoadingTableView) {
        NSLog(@"end loading");
    }
}

- (BOOL)isFinishedLoadingTableView:(UITableView *)tableView 
                         indexPath:(NSIndexPath *)indexPath
{
    // The reason we cannot just look for the last row is because 
    // in iOS7.0x the last row is updated before
    // looping through all the visible rows in ascending order 
    // including the last row again. Strange but true.
    NSArray * visibleRows = [tableView indexPathsForVisibleRows];   // did verify sorted ascending via logging
    NSIndexPath *lastVisibleCellIndexPath = [visibleRows lastObject];
    // For tableviews with multiple sections this will be more complicated.
    BOOL isPreviousCallForPreviousCell = 
             self.previousDisplayedIndexPath.row + 1 == lastVisibleCellIndexPath.row;
    BOOL isLastCell = [indexPath isEqual:lastVisibleCellIndexPath];
    BOOL isFinishedLoadingTableView = isLastCell && isPreviousCallForPreviousCell;
    self.previousDisplayedIndexPath = indexPath;
    return isFinishedLoadingTableView;
}

-3

Tujuan C

[self.tableView reloadData];
[self.tableView performBatchUpdates:^{}
                              completion:^(BOOL finished) {
                                  /// table-view finished reload
                              }];

Cepat

self.tableView?.reloadData()
self.tableView?.performBatchUpdates({ () -> Void in

                            }, completion: { (Bool finished) -> Void in
                                /// table-view finished reload
                            })

1
Metode ini hanya tersedia UICollectionViewsejauh yang saya mengerti.
Hebat-o

Ya kamu benar. BeginUpdates UITableView sama dengan performBatchUpdates UICollectionView: penyelesaian. stackoverflow.com/a/25128583/988169
pkc456

Jadi berikan jawaban yang tepat & valid untuk pertanyaan itu.
G.Abhisek
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.