Hoe het einde van het laden van UITableView te detecteren

Ik wil de offset van de tabel wijzigen wanneer het laden is voltooid en die offset is afhankelijk van het aantal cellen dat op de tabel is geladen.

Is het hoe dan ook op de SDK om te weten wanneer het laden van een uitableview is voltooid? Ik zie niets over gedelegeerde of gegevensbronprotocollen.

Ik kan de telling van de gegevensbronnen niet gebruiken omdat alleen de zichtbare cellen worden geladen.


Antwoord 1, autoriteit 100%

Verbeteren naar @RichX-antwoord:
lastRowkan zowel [tableView numberOfRowsInSection: 0] - 1als ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).rowzijn.
De code wordt dus:

-(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:
Nou, de opmerking van @htafoya klopt. Als u wilt dat deze code het einde van het laden van alle gegevens van de bron detecteert, zou dat niet het geval zijn, maar dat is niet de oorspronkelijke vraag. Deze code is om te detecteren wanneer alle cellen worden weergegeven die zichtbaar moeten zijn. willDisplayCell:wordt hier gebruikt voor een soepelere gebruikersinterface (eencellige wordt meestal snel weergegeven na willDisplay:-aanroep). Je zou het ook kunnen proberen met tableView:didEndDisplayingCell:.


Antwoord 2, autoriteit 19%

Swift 3 & 4 & 5 versie:

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

Antwoord 3, autoriteit 13%

Ik gebruik altijd deze zeer eenvoudige oplossing:

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

Antwoord 4, autoriteit 5%

Hier is nog een andere optie die voor mij lijkt te werken. Controleer in de viewForFooter-delegatiemethode of dit de laatste sectie is en voeg daar uw code toe. Deze aanpak kwam in me op nadat ik me realiseerde dat willDisplayCell geen rekening houdt met voetteksten als je die hebt.

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

Ik vind dat deze aanpak het beste werkt als je op zoek bent naar het eindladen voor de hele UITableView, en niet alleen voor de zichtbare cellen. Afhankelijk van je behoeften wil je misschien alleen de zichtbare cellen, in welk geval folex’s antwoordeen goede route is.


Antwoord 5, autoriteit 5%

Privé-API gebruiken:

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

Openbare API gebruiken:

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

Een mogelijk beter ontwerp is om de zichtbare cellen aan een set toe te voegen, en wanneer je moet controleren of de tabel is geladen, kun je in plaats daarvan een for-lus rond deze set doen, bijvoorbeeld

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

Antwoord 6, autoriteit 5%

Snelle oplossing:

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

Antwoord 7, autoriteit 3%

Voor de gekozen antwoordversie in 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
        }
    }
}

Ik had de variabele isLoadingTableView nodig omdat ik er zeker van wilde zijn dat de tabel klaar was met laden voordat ik een standaard celselectie maakte. Als u dit niet opneemt, wordt uw code elke keer dat u door de tabel bladert opnieuw aangeroepen.


Antwoord 8, autoriteit 3%

De beste aanpak die ik ken, is het antwoord van Eric op: Een melding ontvangen wanneer UITableView klaar is met het vragen om gegevens?

Update: om het te laten werken, moet ik deze aanroepen in -tableView:cellForRowAtIndexPath:

plaatsen

[tableView beginUpdates];
[tableView endUpdates];

Antwoord 9, autoriteit 3%

Om te weten wanneer een tabelweergave klaar is met het laden van de inhoud, moeten we eerst een basiskennis hebben van hoe de weergaven op het scherm worden weergegeven.

In de levenscyclus van een app zijn er 4 sleutelmomenten:

  1. De app ontvangt een gebeurtenis (aanraken, timer, verzonden blokkering enz.)
  2. De app handelt de gebeurtenis af (hij wijzigt een beperking, start een animatie, verandert de achtergrond, enz.)
  3. De app berekent de nieuwe weergavehiërarchie
  4. De app geeft de weergavehiërarchie weer en geeft deze weer

De 2 en 3 tijden zijn totaal gescheiden. Waarom?Om prestatieredenen willen we niet alle berekeningen uitvoeren (gedaan bij 3) elke keer dat een wijziging wordt uitgevoerd.

Dus ik denk dat je met een zaak als deze wordt geconfronteerd:

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

Wat is hier mis?

Een tabelweergave laadt de inhoud traag opnieuw. Als u reloadDatameerdere keren aanroept, ontstaan ​​er eigenlijk geen prestatieproblemen. De tabelweergave berekent alleen de inhoudsgrootte opnieuw op basis van de implementatie van de gedelegeerde en wacht op moment 3 om de cellen te laden. Deze tijd wordt een lay-outpas genoemd.

Ok, hoe doe je mee aan de lay-outpas?

Tijdens de lay-outpas berekent de app alle frames van de weergavehiërarchie. Om mee te doen, kunt u de speciale methoden layoutSubviews, updateLayoutConstraintsenz. overschrijven in een UIViewsubklasse en de equivalente methoden in een view controller subklasse.

Dat is precies wat een tabelweergave doet. Het overschrijft layoutSubviewsen op basis van uw gedelegeerde implementatie voegt het cellen toe of verwijdert het cellen. Het roept cellForRowaan net voor het toevoegen en opmaken van een nieuwe cel, willDisplaydirect daarna. Als je reloadDatahebt aangeroepen of gewoon de tabelweergave aan de hiërarchie hebt toegevoegd, voegt de tabellenweergave zoveel cellen toe als nodig is om het frame op dit belangrijke moment te vullen.

Ok, maar hoe weet u nu wanneer een tabelweergave klaar is met het opnieuw laden van de inhoud?

We kunnen deze vraag herformuleren: hoe weet je wanneer een tabelweergave klaar is met het opmaken van de subweergaven?

De gemakkelijkste manier is om in de lay-out van de tabelweergave te komen:

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

Houd er rekening mee dat deze methode vele malen wordt aangeroepen in de levenscyclus van de tabelweergave. Vanwege het scrollen en het uitstelgedrag van de tabelweergave worden cellen vaak gewijzigd, verwijderd en toegevoegd. Maar het werkt, direct na de super.layoutSubviews()worden cellen geladen. Deze oplossing komt overeen met het wachten op de gebeurtenis willDisplayvan het laatste indexpad. Deze gebeurtenis wordt aangeroepen tijdens de uitvoering van layoutSubviewsvan de tabelweergave wanneer een cel wordt toegevoegd.

Een andere manier is om een ​​melding te krijgen wanneer de app een lay-outpas voltooit.

Zoals beschreven in de documentatie, kan een optie gebruiken van de UIView.animate(withDuration:completion):

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

Deze oplossing werkt, maar het scherm wordt één keer vernieuwd tussen het moment dat de lay-out klaar is en het moment dat het blok wordt uitgevoerd. Dit is gelijk aan de DispatchMain.async-oplossing, maar gespecificeerd.

Als alternatief zou ik de indeling van de tabelweergave liever forceren

Er is een speciale methode om elke weergave te dwingen de subweergaveframes onmiddellijk te berekenen layoutIfNeeded:

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

Wees echter voorzichtig, als u dit doet, wordt de luie belasting die door het systeem wordt gebruikt, verwijderd. Het herhaaldelijk aanroepen van deze methoden kan prestatieproblemen veroorzaken. Zorg ervoor dat ze niet worden aangeroepen voordat het frame van de tabelweergave is berekend, anders wordt de tabelweergave opnieuw geladen en ontvangt u geen melding.


Ik denk dat er geen perfecte oplossing is. Het onderklassen van klassen kan leiden tot trubles. Een lay-outpas begint vanaf de bovenkant en gaat naar de onderkant, dus het is niet eenvoudig om een ​​melding te krijgen wanneer alle lay-out klaar is. En layoutIfNeeded()kan prestatieproblemen veroorzaken, enz.


Antwoord 10

Zo doe je het in 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
    }
    // ....
}

Antwoord 11

hier is hoe ik het doe in 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()
    }
}

Antwoord 12

Dit is wat ik zou doen.

  1. In uw basisklasse (kan rootVC BaseVc enz. zijn),

    A. Schrijf een protocol om de “DidFinishReloading”-callback te verzenden.

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

    B. Schrijf een generieke methode om de tabelweergave opnieuw te laden.

    -(void)reloadTableView:(UITableView *)tableView withOwner:(UIViewController *)aViewController;
    
  2. In de implementatie van de basisklassemethode roept u reloadData aan gevolgd door delegateMethod met vertraging.

    -(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. Bevestig het protocol voor het voltooien van het herladen in alle weergavecontrollers waar u de callback nodig hebt.

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

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


Antwoord 13

Ik kopieer de code van Andrew en breid deze uit om rekening te houden met het geval dat je slechts 1 rij in de tabel hebt. Het werkt tot nu toe voor mij!

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

OPMERKING: ik gebruik slechts 1 sectie uit de code van Andrew, dus houd daar rekening mee..


Antwoord 14

@folex antwoord is juist.

Maar het zal mislukkenals de tableView meer dan één sectietegelijk heeft weergegeven.

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

Antwoord 15

In Swift kun je zoiets doen. De volgende voorwaarde is waar elke keer dat u het einde van de tabel bereikt. Bekijk

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

Antwoord 16

Als je meerdere secties hebt, kun je als volgt de laatste rij in de laatste sectie krijgen (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
        }
    }
}

Antwoord 17

Heel toevallig kwam ik deze oplossing tegen:

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

U moet de footerView instellen voordat u de contentSize krijgt, b.v. in weergaveDidLoad.
Trouwens. door footeView in te stellen kunt u “ongebruikte” scheidingstekens verwijderen


Antwoord 18

Bent u op zoek naar het totale aantal items dat in de tabel wordt weergegeven of het totale aantal items dat momenteel zichtbaar is? Hoe dan ook.. Ik geloof dat de ‘viewDidLoad’-methode wordt uitgevoerd nadat alle gegevensbronmethoden zijn aangeroepen. Dit werkt echter alleen bij de eerste lading van de gegevens (als u een enkele alloc ViewController gebruikt).


Antwoord 19

Ik weet dat dit is beantwoord, ik voeg alleen maar een aanbeveling toe.

Volgens de volgende documentatie

https://www.objc.io/issues /2-concurrency/thread-safe-class-design/

Het oplossen van timingproblemen met dispatch_async is een slecht idee. Ik stel voor dat we dit moeten aanpakken door FLAG of zoiets toe te voegen.


Antwoord 20

In iOS7.0x is de oplossing een beetje anders. Dit is wat ik bedacht.

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

Antwoord 21

Doelstelling C

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

Snel

self.tableView?.reloadData()
self.tableView?.performBatchUpdates({ () -> Void in
                            }, completion: { (Bool finished) -> Void in
                                /// table-view finished reload
                            })

Other episodes