2009-06-29 5 views
37

AggiornamentoUITableView: l'eliminazione di sezioni con animazione

mi hanno inviato la mia soluzione a questo problema come una risposta di seguito. Ci vuole un approccio diverso dalla mia prima revisione.


domanda originale Ho già fatto una domanda sul SO che ho pensato risolto i miei problemi:

How to deal with non-visible rows during row deletion. (UITableViews)

Tuttavia, ora ho di nuovo problemi simili durante la rimozione di sezioni da un UITableView. (sono riemersi quando ho variato il numero di sezioni/righe nella tabella).

Prima di perderti a causa della lunghezza di taglio del mio post, permettimi di indicare chiaramente il problema e puoi leggere tutto il tempo necessario per fornire una risposta.


Problema:

Se lotti eliminazione di righe e sezioni da un UITableView, l'applicazione si blocca, a volte. Dipende dalla configurazione della tabella e dalla combinazione di righe e sezioni che scelgo di rimuovere.

Il registro dice sono caduto perché dice che non ho aggiornato l'origine dati e la tabella correttamente:

Invalid update: invalid number of rows in section 5. The number of rows contained in an existing section after the update (2) must be equal to the number of rows contained in that section before the update (1), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted). 

Ora in fretta, prima di scrivere la risposta ovvia, vi assicuro che ho davvero aggiunto e cancellato il righe e sezioni correttamente da dataSource. La spiegazione è lunga, ma la troverai sotto, seguendo il metodo.

Quindi, con questo, se siete ancora interessati ...


metodo che gestisce la rimozione delle sezioni e righe:

- (void)createFilteredTableGroups{ 

    //index set to hold sections to remove for deletion animation 
    NSMutableIndexSet *sectionsToDelete = [NSMutableIndexSet indexSet]; 
    [sectionsToDelete removeIndex:0]; 


    //array to track cells for deletion animation 
    NSMutableArray *cellsToDelete = [NSMutableArray array]; 

    //array to track controllers to delete from presentation model 
    NSMutableArray *controllersToDelete = [NSMutableArray array]; 

    //for each section 
    for(NSUInteger i=0; i<[tableGroups count];i++){ 

     NSMutableArray *section = [tableGroups objectAtIndex:i]; 

     //controllers to remove 
     NSMutableIndexSet *controllersToDeleteInCurrentSection = [NSMutableIndexSet indexSet]; 
     [controllersToDeleteInCurrentSection removeIndex:0]; 
     NSUInteger indexOfController = 0; 

     //for each cell controller 
     for(ScheduleCellController *cellController in section){ 

      //bool indicating whether the cell controller's cell should be removed 
      NSString *shouldDisplayString = (NSString*)[[cellController model] objectForKey:@"filteredDataSet"]; 
      BOOL shouldDisplay = [shouldDisplayString boolValue]; 

      //if it should be removed 
      if(!shouldDisplay){ 

       NSIndexPath *cellPath = [self indexPathOfCellWithCellController:cellController]; 

       //if cell is on screen, mark for animated deletion 
       if(cellPath!=nil) 
        [cellsToDelete addObject:cellPath]; 

       //marking controller for deleting from presentation model 
       [controllersToDeleteInCurrentSection addIndex:indexOfController];     

      } 
      indexOfController++; 
     } 

     //if removing all items in section, add section to removed in animation 
     if([controllersToDeleteInCurrentSection count]==[section count]) 
      [sectionsToDelete addIndex:i]; 

     [controllersToDelete addObject:controllersToDeleteInCurrentSection]; 

    } 


    //copy the unfiltered data so we can remove the data that we want to filter out 
    NSMutableArray *newHeaders = [tableHeaders mutableCopy]; 
    NSMutableArray *newTableGroups = [[allTableGroups mutableCopy] autorelease]; 


    //removing controllers 
    int i = 0; 
    for(NSMutableArray *section in newTableGroups){ 
     NSIndexSet *indexesToDelete = [controllersToDelete objectAtIndex:i]; 
     [section removeObjectsAtIndexes:indexesToDelete]; 
     i++; 
    } 

    //removing empty sections and cooresponding headers 
    [newHeaders removeObjectsAtIndexes:sectionsToDelete]; 
    [newTableGroups removeObjectsAtIndexes:sectionsToDelete]; 

    //update headers 
    [tableHeaders release]; 
    tableHeaders = newHeaders; 

    //storing filtered table groups 
    self.filteredTableGroups = newTableGroups; 


    //filtering animation and presentation model update 
    [self.tableView beginUpdates]; 
    tableGroups = self.filteredTableGroups; 
    [self.tableView deleteSections:sectionsToDelete withRowAnimation:UITableViewRowAnimationTop]; 
    [self.tableView deleteRowsAtIndexPaths:cellsToDelete withRowAnimation:UITableViewRowAnimationTop]; 
    [self.tableView endUpdates]; 


    //marking table as filtered 
    self.tableIsFiltered = YES; 


} 

mia ipotesi:

Il problema Questo sembra essere questo: se guardi sopra dove elencho il numero di celle in ogni sezione, vedrai che la sezione 5 sembra aumentare di 1. Tuttavia, questo non è vero. La sezione originale 5 è stata effettivamente cancellata e un'altra sezione ha preso il suo posto (in particolare, è la vecchia sezione 10).

Quindi perché la vista del tavolo sembra non rendersene conto? È necessario SAPERE che ho rimosso la vecchia sezione e non dovremmo aspettarci che una nuova sezione che ora si trova nell'indice della vecchia sezione sia vincolata dal numero di righe della sezione eliminata.

Speriamo che questo abbia senso, è un po 'complicato scrivere questo.

(notare questo codice ha funzionato prima con un diverso numero di righe/sezioni.questa particolare configurazione sembra dargli problemi)

risposta

87

Ho incontrato questo problema prima. Stai tentando di eliminare tutte le righe da una sezione e, in aggiunta, quella sezione ora vuota. Tuttavia, è sufficiente (e corretto) rimuovere solo quella sezione. Verranno rimosse anche tutte le righe al suo interno. Ecco alcuni esempi di codice dal mio progetto che gestisce l'eliminazione di una riga. Ha bisogno di determinare se deve solo rimuovere questa riga da una sezione o cancellare l'intera sezione se è l'ultima riga rimanente in quella sezione:

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath 
{ 
    if (editingStyle == UITableViewCellEditingStyleDelete) 
    { 
     // modelForSection is a custom model object that holds items for this section. 
     [modelForSection removeItem:[self itemForRowAtIndexPath:indexPath]]; 

     [tableView beginUpdates]; 

     // Either delete some rows within a section (leaving at least one) or the entire section. 
     if ([modelForSection.items count] > 0) 
     { 
      // Section is not yet empty, so delete only the current row. 
      [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] 
          withRowAnimation:UITableViewRowAnimationFade]; 
     } 
     else 
     { 
      // Section is now completely empty, so delete the entire section. 
      [tableView deleteSections:[NSIndexSet indexSetWithIndex:indexPath.section] 
        withRowAnimation:UITableViewRowAnimationFade]; 
     } 

     [tableView endUpdates]; 
    } 
} 
4

Ho notato che stai eliminando prima le sezioni dalla tabella e poi eliminando le righe.

So che c'è uno complicated discussion of batch insertion and deletion per UITableViews nella Guida alla programmazione di Table View, ma non lo copre in modo specifico.

Penso che quello che sta succedendo è che l'eliminazione delle sezioni sta facendo sì che le cancellazioni di riga si riferiscano alla riga sbagliata.

vale a dire che si desidera eliminare la sezione 2 e la riga 1 dalla sezione 4 ... ma dopo aver eliminato la sezione 2, la vecchia sezione 4 è ora la terza, quindi quando si elimina il vecchio NSIndexPath di (4, 1) stai cancellando alcune righe casuali che potrebbero non esistere.

Quindi penso che la correzione potrebbe essere semplice come lo scambio di quelle due linee di codice, quindi stai cancellando prima le righe, poi le sezioni.

+0

In alternativa, tenere traccia di IndexPath per ogni cella di cui si ha bisogno e correggerli in modo appropriato man mano che si eliminano le eliminazioni. (Questo potrebbe essere il modo lungo/contorto/inappropriato di farlo - solo un pensiero.) – Tim

+0

Sto facendo un batch di eliminazione, quindi non fa differenza nell'ordine in cui elenco le operazioni. La vista tabella esegue le operazioni "alla volta" quando si trovano all'interno del blocco di aggiornamento. Come ero paranoico, ho provato a cambiare l'ordine delle operazioni senza successo. La numerazione delle sezioni/righe non cambia (non deve cambiare) durante l'eliminazione del batch. Se non stavi usando i blocchi, avresti ragione. –

+0

@Tim Pensiero interessante. Hai ragione, potrebbe essere piuttosto noioso con una grande quantità di cancellazioni (che avrò). Mi chiedo anche se potrei fare più eliminazioni in rapida successione. Stavo cercando di fare eliminazioni in batch per evitare questi problemi, ma potrebbe essere necessario. –

3

Così alla fine ecco la mia soluzione a questo problema. Questo metodo può essere applicato a tabelle di qualsiasi dimensione, qualsiasi numero di sezioni (per quanto ne so)

Come prima ho modificato il codice tableview di Matt Gallagher che colloca la logica specifica della cella in un controller di cella separato. Tuttavia, si può facilmente adattare questo metodo per un modello diverso

ho aggiunto le seguenti (rilevanti) Ivars al codice del Matt:

NSArray *allTableGroups; //always has a copy of every cell controller, even if filtered 
NSArray *filteredTableGroups; //always has a copy of the filtered table groups 

Ivar originale di Matt:

NSArray *allTableGroups 

... sempre punti a uno degli array di cui sopra.

Questo può probabilmente essere rifattorizzato e migliorato in modo significativo, ma non ne ho avuto la necessità. Inoltre, se si utilizzano i dati principali, NSFetchedResultsController semplifica la procedura.

Ora al metodo (che sto cercando di lasciare un commento, per quanto mi è possibile):

- (void)createFilteredTableGroups{ 

    //Checking for the usual suspects. all which may through an exception 
    if(model==nil) 
     return; 
    if(tableGroups==nil) 
     return; 
    if([tableGroups count]==0) 
     return; 


    //lets make a new array to work with 
    NSMutableArray *newTableGroups = [[allTableGroups mutableCopy] autorelease]; 

    //telling the table what we are about to do 
    [self.tableView beginUpdates]; 


    //array to track cells for deletion animation 
    NSMutableArray *indexesToRemove = [NSMutableArray array]; 

    //loop through each section 
    for(NSMutableArray *eachSection in tableGroups){ 

     //keeping track of the indexes to delete for each section 
     NSMutableIndexSet *indexesForSection = [NSMutableIndexSet indexSet]; 
     [indexesForSection removeAllIndexes]; 

     //increment though cell indexes 
     int rowIndex = 0; 

     //loop through each cellController in the section 
     for(ScheduleCellController *eachCellController in eachSection){ 

      //Ah ha! A little magic. the cell controller must know if it should be displayed. 
      //This you must calculate in your business logic 
      if(![eachCellController shouldDisplay]){ 

       //add non-displayed cell indexes 
       [indexesForSection addIndex:rowIndex]; 

      } 
      rowIndex++; 
     } 
     //adding each array of section indexes, EVEN if it is empty (no indexes to delete) 
     [indexesToRemove addObject:indexesForSection]; 

    } 

    //Now we remove cell controllers in newTableGroups and cells from the table 
    //Also, each subarray of newTableGroups is mutable as well 
    if([indexesToRemove count]>0){ 

     int sectionIndex = 0; 
     for(NSMutableIndexSet *eachSectionIndexes in indexesToRemove){ 

      //Now you know why we stuck the indexes into individual arrays, easy array method 
      [[newTableGroups objectAtIndex:sectionIndex] removeObjectsAtIndexes:eachSectionIndexes]; 

      //tracking which cell indexPaths to remove for each section 
      NSMutableArray *indexPathsToRemove = [NSMutableArray array]; 
      int numberOfIndexes = [eachSectionIndexes count]; 

      //create array of indexPaths to remove 
      NSUInteger index = [eachSectionIndexes firstIndex]; 
      for(int i = 0; i< numberOfIndexes; i++){ 

       NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:sectionIndex]; 
       [indexPathsToRemove addObject:indexPath]; 
       index = [eachSectionIndexes indexGreaterThanIndex:index]; 
      } 

      //delete the rows for this section 
      [self.tableView deleteRowsAtIndexPaths:indexPathsToRemove withRowAnimation:UITableViewRowAnimationTop]; 

      //next section please 
      sectionIndex++; 
     } 

    } 

    //now we figure out if we need to remove any sections 
    NSMutableIndexSet *sectionsToRemove = [NSMutableIndexSet indexSet]; 
    [sectionsToRemove removeAllIndexes]; 

    int sectionsIndex = 0; 
    for(NSArray *eachSection in newTableGroups){ 

     //checking for empty sections 
     if([eachSection count]==0) 
      [sectionsToRemove addIndex:sectionsIndex]; 

     sectionsIndex++; 
    } 

    //updating the table groups 
    [newTableGroups removeObjectsAtIndexes:sectionsToRemove]; 

    //removing the empty sections 
    [self.tableView deleteSections:sectionsToRemove withRowAnimation:UITableViewRowAnimationTop]; 

    //updating filteredTableGroups to the newTableGroups we just created 
    self.filteredTableGroups = newTableGroups; 

    //pointing tableGroups at the filteredGroups 
    tableGroups = filteredTableGroups; 

    //invokes the animation 
    [self.tableView endUpdates]; 


} 
1

Ho visto questo stesso errore esatto come il risultato di liberare prematuramente la vista della mia cella Tableview personalizzato sfondo.

Con NSZombieEnabled ho ottenuto un'eccezione che viene lanciata molto più in basso di una chiamata interna a una funzione per preparare la cella per il riutilizzo. Senza NSZombieEnabled, ricevevo l'errore di coerenza interna.

Incidentalmente quando ho risolto il problema di mantenimento/rilascio sulla vista di sfondo della cella, sono stato in grado di eliminare l'ultima riga della sezione senza dover eliminare esplicitamente la sezione.

Morale della storia: Questo errore significa solo qualcosa di brutto sta accadendo quando si tenta di eliminare, e una delle cose che accade quando si elimina è la cellula viene preparazione per il riutilizzo, quindi se si sta facendo qualcosa di personalizzato con il vostro celle tableview, cerca un possibile errore lì.

0

o semplicemente fare questo

- (void)tableView:(UITableView *)tv  
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle 
forRowAtIndexPath:(NSIndexPath *)indexPath { 

if(editingStyle == UITableViewCellEditingStyleDelete) {  
    //Delete the object from the table. 
    [directoriesOfFolder removeObjectAtIndex:indexPath.row]; 
    [tv deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] 
withRowAnimation:UITableViewRowAnimationFade]; 
} 
} 

directory di cartella essere il tuo Array! Questo è tutto sopra i codici non ha funzionato per me! Questo è meno costoso da fare e ha senso!

2

Sospetto che si stia dimenticando di rimuovere l'oggetto che rappresenta la sezione dalla memoria interna, in modo che il metodo -numberOfSectionsInTableView: restituisca ancora 1 dopo che tutte le sezioni sono state eliminate.

Questo è esattamente quello che stavo facendo male quando ho avuto lo stesso incidente!

1

Un modo molto più semplice per affrontare questo è quello di aggiornare l'origine dati, quindi chiamare reloadSections

[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationFade]; 

Questo sarà ricaricata una singola sezione. In alternativa è possibile utilizzare indexSetWithIndexesInRange: per ricaricare più sezioni contemporaneamente.