Jak wykryć koniec ładowania UITableView


141

Chcę zmienić przesunięcie tabeli po zakończeniu ładowania, a przesunięcie to zależy od liczby komórek załadowanych do tabeli.

Czy mimo to w SDK jest informacja, kiedy zakończyło się ładowanie uitableview? Nie widzę nic ani na delegacie, ani w protokołach źródła danych.

Nie mogę użyć liczby źródeł danych z powodu ładowania tylko widocznych komórek.


wypróbuj kombinację liczby źródeł danych i „indexPathsForVisibleRows”
Swapnil Luktuke

1
Ponownie otwarte na podstawie dalszych informacji we fladze: "To nie jest duplikat. Pyta o załadowanie widocznych komórek, a nie o zakończenie pytania o dane. Zobacz aktualizację do zaakceptowanej odpowiedzi"
Kev

To rozwiązanie działa dobrze dla mnie. Sprawdzasz stackoverflow.com/questions/1483581/…
Khaled Annajar

Odpowiedzi:


224

Popraw do @RichX odpowiedź: lastRowmoże to być oba [tableView numberOfRowsInSection: 0] - 1lub ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).row. Więc kod będzie wyglądał następująco:

-(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];
    }
}

AKTUALIZACJA: Cóż, komentarz @ htafoya jest słuszny. Jeśli chcesz, aby ten kod wykrył koniec ładowania wszystkich danych ze źródła, nie zrobiłby tego, ale to nie jest oryginalne pytanie. Ten kod służy do wykrywania, kiedy wyświetlane są wszystkie komórki, które mają być widoczne. willDisplayCell:używany tutaj dla płynniejszego interfejsu użytkownika (pojedyncza komórka zwykle wyświetla się szybko po willDisplay:wywołaniu). Możesz też spróbować tableView:didEndDisplayingCell:.


Znacznie lepiej jest wiedzieć, kiedy ładują się wszystkie widoczne ogniwa.
Eric

14
Jednak będzie to wywoływane za każdym razem, gdy użytkownik przewinie, aby wyświetlić więcej komórek.
htafoya

Problem polega na tym, że nie uwzględnia widoku stopki. Dla większości może to nie stanowić problemu, ale jeśli tak, możesz zastosować ten sam pomysł, ale w viewForFooterInSectionmetodzie.
Kyle Clegg,

1
@KyleClegg patric.schenke wspomniał o jednym z powodów w komentarzu do Twojej odpowiedzi. A co, jeśli stopka nie jest widoczna na końcu ładowania komórki?
folex

27
tableView: didEndDisplayingCell: jest faktycznie wywoływana podczas usuwania komórki z widoku, a nie po zakończeniu renderowania w widoku, więc to nie zadziała. Nie jest to dobra nazwa metody. Muszę przeczytać dokumenty.
Andrew Raphael,

34

Wersja Swift 3 i 4 i 5:

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

1
ta metoda jest wywoływana podczas pierwszego ładowania danych. W moim przypadku widoczne są tylko 4 komórki. Załadowałem 8 wierszy, ale nadal ta funkcja była wywoływana. Chcę załadować więcej danych, gdy użytkownik przewinie w dół do ostatniego wiersza
Muhammad Nayab,

Cześć Muhammad Nayab, może mógłbyś zamienić „if indexPath == lastVisibleIndexPath” na „if indexPath.row == yourDataSource.count”
Daniele Ceglia

1
@DanieleCeglia Dzięki za zwrócenie uwagi. if indexPath == lastVisibleIndexPath && indexPath.row == yourDataSource.count - 1 to będzie działać dla mnie. Twoje zdrowie!!!
Yogesh Patel

28

Zawsze używam tego bardzo prostego rozwiązania:

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

jak wykryć ostatni wiersz? to jest problem dla mnie… czy możesz wyjaśnić, jak ostatnio doprowadziłeś do takiego stanu.
Sameera Chathuranga,

6
Ostatni wiersz to [tableView numberOfRowsInSection: 0] - 1. Musisz zastąpić 0wymaganą wartością. Ale to nie jest problem. Problem polega na tym, że UITableView ładuje się tylko jako widoczny. Jednak ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).rowrozwiązuje problem.
folex

1
Jeśli szukamy absolutnego zakończenia, czy nie byłoby najlepiej użyć - (void) tableView: (UITableView *) tableView didEndDisplayingCell: (UITableViewCell *) komórka forRowAtIndexPath: (NSIndexPath *) indexPath?
morcutt

10

Oto inna opcja, która wydaje się działać dla mnie. W metodzie delegata viewForFooter sprawdź, czy jest to ostatnia sekcja i dodaj tam swój kod. Takie podejście przyszło mi do głowy po zdaniu sobie sprawy, że willDisplayCell nie uwzględnia stopek, jeśli je masz.

- (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");
}

Uważam, że to podejście działa najlepiej, jeśli szukasz końcowego obciążenia dla całego UITableView, a nie tylko widocznych komórek. W zależności od potrzeb możesz chcieć tylko widocznych komórek, w takim przypadku odpowiedzią folexu jest dobra trasa.


Wracając nilz tableView:viewForFooterInSection:bałaganu z moim układem w iOS 7 przy użyciu Auto Layout.
ma11hew28

Ciekawy. A co, jeśli ustawisz go na ramkę o wysokości i szerokości 0? return CGRectMake(0,0,0,0);
Kyle Clegg

Próbowałem tego, a nawet ustawiłem self.tableView.sectionFooterHeight = 0. Tak czy inaczej, wydaje się, że wstawiany jest widok stopki sekcji o wysokości około 10. Założę się, że mógłbym to naprawić, dodając ograniczenie wysokości 0 do widoku, który zwracam. W każdym razie jestem dobry, ponieważ tak naprawdę chciałem dowiedzieć się, jak uruchomić UITableView na ostatniej komórce, ale zobaczyłem to jako pierwszy.
ma11hew28

1
@MattDiPasquale, jeśli zaimplementujesz viewForFooterInSection, musisz również zaimplementować heightForFooterInSection. Musi zwrócić 0 dla sekcji z zerową stopką. Jest to teraz również w oficjalnych dokumentach.
patric.schenke

viewForFooterInSection nie zostanie wywołany, jeśli ustawisz heightForFooterInSection na 0
Oded Harth

10

Rozwiązanie 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

Korzystanie z prywatnego interfejsu API:

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

Korzystanie z publicznego interfejsu API:

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

Możliwym lepszym projektem jest dodanie widocznych komórek do zestawu, wtedy gdy trzeba sprawdzić, czy tabela jest załadowana, można zamiast tego zrobić pętlę for wokół tego zestawu, np.

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

Znacznie lepsze rozwiązanie niż inne. Nie wymaga przeładowania tableView. Jest to bardzo proste, a viewDidLoadRows: nie jest wywoływany za każdym razem, gdy ładowana jest ostatnia komórka.
Matt Hudson,

jest to jak dotąd najlepsze rozwiązanie, ponieważ inne rozwiązania nie uwzględniają wielu sekcji
Justicepenny

Przepraszam za ten złośliwy komentarz, ale na czym dokładnie polega ta „magia”? Wywołanie innej metody od delegata. Nie rozumiem.
Lukas Petr

@LukasPetr spójrz na anulowanie i afterDelay. Umożliwiają one anulowanie wywołania metody dla każdej sekcji z wyjątkiem ostatniej, czyli wtedy, gdy tabela została całkowicie załadowana.
malhal

@malhal oh, okay. Jest trochę sprytniejszy, niż myślałem na pierwszy rzut oka.
Lukas Petr

8

Dla wybranej wersji odpowiedzi w 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
        }
    }
}

Potrzebowałem zmiennej isLoadingTableView, ponieważ chciałem się upewnić, że tabela została załadowana przed dokonaniem domyślnego wyboru komórki. Jeśli tego nie uwzględnisz, za każdym razem, gdy przewiniesz tabelę, ponownie wywoła twój kod.



3

Aby wiedzieć, kiedy widok tabeli kończy ładowanie zawartości, najpierw musimy mieć podstawową wiedzę o tym, jak widoki są wyświetlane na ekranie.

W cyklu życia aplikacji istnieją 4 kluczowe momenty:

  1. Aplikacja odbiera zdarzenie (dotyk, licznik czasu, wysłanie bloku itp.)
  2. Aplikacja obsługuje zdarzenie (modyfikuje ograniczenie, uruchamia animację, zmienia tło itp.)
  3. Aplikacja oblicza nową hierarchię widoków
  4. Aplikacja renderuje hierarchię widoków i wyświetla ją

Czasy 2 i 3 są całkowicie oddzielone. Czemu ? Ze względu na wydajność nie chcemy wykonywać wszystkich obliczeń w momencie 3 za każdym razem, gdy wprowadzana jest modyfikacja.

Myślę więc, że masz do czynienia z takim przypadkiem:

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

Co tu jest nie tak?

Jak każdy widok, widok tabeli leniwie ponownie ładuje zawartość. Właściwie, jeśli zadzwonisz reloadDatawiele razy, nie spowoduje to problemów z wydajnością. Widok tabeli tylko ponownie oblicza swój rozmiar zawartości na podstawie swojej implementacji delegata i czeka na moment 3, aby załadować swoje komórki. Ten czas nazywa się przebiegiem układu.

OK, jak dostać się do karty układu?

Podczas przebiegu układu aplikacja oblicza wszystkie klatki hierarchii widoków. Aby wziąć udział, można zastąpić dedykowane metody layoutSubviews, updateLayoutConstraintsetc w krótkim czasie UIVieworaz równoważne metody w podklasie widok kontrolera.

Dokładnie to robi widok tabeli. Zastępuje layoutSubviewsi na podstawie implementacji delegata dodaje lub usuwa komórki. Wywołuje cellForRowbezpośrednio przed dodaniem i ułożeniem nowej komórki, willDisplayzaraz po. Jeśli wywołałeś reloadDatalub właśnie dodałeś widok tabeli do hierarchii, widok tabel dodaje tyle komórek, ile potrzeba, aby wypełnić ramkę w tym kluczowym momencie.

W porządku, ale teraz, jak sprawdzić, kiedy widok tabel zakończył ponowne ładowanie zawartości?

Możemy teraz przeformułować to pytanie: skąd wiedzieć, kiedy widok tabeli skończył układać podglądy?

Najłatwiej jest wejść do układu widoku tabeli:

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

Zwróć uwagę, że ta metoda jest wywoływana wiele razy w cyklu życia widoku tabeli. Ze względu na przewijanie i usuwanie z kolejki widoku tabeli komórki są często modyfikowane, usuwane i dodawane. Ale to działa zaraz po super.layoutSubviews()załadowaniu komórek. To rozwiązanie jest równoważne oczekiwaniu na willDisplayzdarzenie ostatniej ścieżki indeksu. To zdarzenie jest wywoływane podczas wykonywania layoutSubviewswidoku tabeli dla każdej dodanej komórki.

Innym sposobem jest wywołanie, gdy aplikacja zakończy przebieg układu.

Jak opisano w dokumentacji , możesz skorzystać z opcji UIView.animate(withDuration:completion):

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

To rozwiązanie działa, ale ekran odświeży się jeden raz między utworzeniem układu a wywołaniem bloku. Jest to równoważne DispatchMain.asyncrozwiązaniu, ale określone.

Alternatywnie wolałbym wymusić układ widoku tabeli

Istnieje specjalna metoda wymuszania natychmiastowego obliczenia ramek widoku podrzędnego w dowolnym widoku layoutIfNeeded:

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

Należy jednak zachować ostrożność, ponieważ spowoduje to usunięcie leniwego ładowania używanego przez system. Wielokrotne wywoływanie tych metod może powodować problemy z wydajnością. Upewnij się, że nie zostaną one wywołane przed obliczeniem ramki widoku tabeli, w przeciwnym razie widok tabeli zostanie ponownie załadowany i nie zostaniesz o tym powiadomiony.


Myślę, że nie ma idealnego rozwiązania. Tworzenie podklas może prowadzić do trubli. Przebieg układu zaczyna się od góry i przechodzi do dołu, więc nie jest łatwo otrzymać powiadomienie, gdy cały układ jest gotowy. I layoutIfNeeded()może powodować problemy z wydajnością itp. Ale znając te opcje, powinieneś być w stanie pomyśleć o jednej alternatywie, która będzie pasować do Twoich potrzeb.


1

Oto jak to się robi w 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

oto jak to robię w 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

Oto co bym zrobił.

  1. W twojej klasie bazowej (może to być rootVC BaseVc itp.),

    A. Napisz protokół, aby wysłać wywołanie zwrotne „DidFinishReloading”.

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

    B. Napisz ogólną metodę ponownego ładowania widoku tabeli.

    -(void)reloadTableView:(UITableView *)tableView withOwner:(UIViewController *)aViewController;
  2. W implementacji metody klasy bazowej wywołaj reloadData, a następnie delegateMethod z opóźnieniem.

    -(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. Potwierdź protokół zakończenia przeładowania we wszystkich kontrolerach widoku, w których potrzebujesz wywołania zwrotnego.

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

Źródła: https://discussions.apple.com/thread/2598339?start=0&tstart=0


0

Odpowiedź @folex jest prawidłowa.

Ale zakończy się niepowodzeniem, jeśli tableView ma więcej niż jedną sekcję wyświetlaną jednocześnie.

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

 }
}

0

W Swift możesz zrobić coś takiego. Następujący warunek będzie spełniony za każdym razem, gdy osiągniesz koniec tableView

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


0

Jeśli masz wiele sekcji, oto jak uzyskać ostatni wiersz w ostatniej sekcji (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

Dość przypadkowo wpadłem na to rozwiązanie:

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

Musisz ustawić footerView przed pobraniem contentSize, np. W viewDidLoad. Przy okazji. ustawienie footeView pozwala usunąć „nieużywane” separatory


-1

Czy szukasz łącznej liczby pozycji, które zostaną wyświetlone w tabeli, czy łącznej liczby aktualnie widocznych pozycji? Tak czy inaczej… Uważam, że metoda „viewDidLoad” jest wykonywana po wywołaniu wszystkich metod źródła danych. Jednak będzie to działać tylko przy pierwszym załadowaniu danych (jeśli używasz jednego alokowanego ViewController).


-1

Kopiuję kod Andrew i rozszerzam go, aby uwzględnić przypadek, w którym masz tylko 1 wiersz w tabeli. Jak dotąd to dla mnie działa!

- (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];
}
}

UWAGA: używam tylko 1 sekcji z kodu Andrew, więc miej to na uwadze.


Witamy w SO @ jpage4500. Zredagowałem twoją odpowiedź, aby usunąć informacje o niskich punktach rep i pytaniu. Rozwinięcie odpowiedzi innego użytkownika jest samo w sobie w pełni poprawną odpowiedzią, więc możesz zmniejszyć bałagan, pomijając te elementy. Dobrze, że przyznałeś Andrew kredyt, więc zostało.
skrrgwasme

-3

W iOS7.0x rozwiązanie jest nieco inne. Oto co wymyśliłem.

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

Cel C

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

Szybki

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

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

1
Ta metoda jest dostępna tylko o UICollectionViewile rozumiem.
Niesamowite-o

Tak masz rację. BeginUpdates UITableView jest równa performBatchUpdates: Complete UICollectionView. stackoverflow.com/a/25128583/988169
pkc456

Więc podaj poprawną i ważną odpowiedź na pytanie.
G.Abhisek
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.