40 votes

UITableView : suppression de sections avec animation

Mise à jour

J'ai publié ma solution à ce problème sous forme de réponse ci-dessous. Elle adopte une approche différente de ma première révision.


Question originale J'ai déjà posé une question sur SO qui, selon moi, a résolu mes problèmes :

http://stackoverflow.com/questions/998603/how-to-deal-with-non-visible-rows-during-row-deletion-uitableviews

Cependant, je rencontre à nouveau des problèmes similaires lorsque je supprime des sections d'un UITableView. (ils ont refait surface lorsque j'ai fait varier le nombre de sections/rangs dans le tableau).

Avant que je ne vous perde à cause de la longueur de mon message, laissez-moi énoncer clairement le problème, et vous pourrez lire autant que vous le souhaitez pour y répondre.


Problème :

Si l'on supprime par lots des lignes ET des sections d'une UITableView, l'application se bloque, parfois. Cela dépend de la configuration de la table et de la combinaison de lignes et de sections que je choisis de supprimer.

Le journal indique que je me suis écrasé parce que je n'ai pas mis à jour la source de données et la table correctement :

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

Maintenant, rapidement, avant que vous n'écriviez la réponse évidente, je vous assure que j'ai bien ajouté et supprimé les lignes et les sections correctement de la source de données. L'explication est longue, mais vous la trouverez ci-dessous, en suivant la méthode.

Donc avec ça, si vous êtes toujours intéressés


Méthode qui gère la suppression des sections et des rangées :

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

}

A mon avis :

Le problème semble être le suivant : Si vous regardez ci-dessus où j'énumère le nombre de cellules dans chaque section, vous verrez que la section 5 semble augmenter de 1. Cependant, ce n'est pas vrai. La section 5 originale a en fait été supprimée et une autre section a pris sa place (plus précisément, il s'agit de l'ancienne section 10).

Alors pourquoi la vue de la table ne semble-t-elle pas s'en rendre compte ? Il devrait SAVOIR que j'ai supprimé l'ancienne section et ne doit pas s'attendre à ce qu'une nouvelle section, qui se trouve maintenant à l'index de l'ancienne section, soit liée par le nombre de lignes de la section supprimée.

J'espère que cela a un sens, c'est un peu compliqué d'écrire tout cela.

(Notez que ce code fonctionnait auparavant avec un nombre différent de lignes/sections. Cette configuration particulière semble lui poser des problèmes).

88voto

Martin Winter Points 800

J'ai déjà rencontré ce problème. Vous essayez de supprimer toutes les lignes d'une section et ensuite, en plus, cette section maintenant vide. Cependant, il est suffisant (et approprié) de ne supprimer que cette section. Toutes les lignes qu'elle contient seront également supprimées. Voici un exemple de code issu de mon projet qui gère la suppression d'une ligne. Il doit déterminer s'il doit supprimer uniquement cette ligne d'une section ou supprimer la section entière s'il s'agit de la dernière ligne restante dans cette section :

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

4voto

David Maymudes Points 5153

Je remarque que vous supprimez d'abord les sections du tableau, puis les lignes.

Je sais qu'il y a un discussion compliquée sur l'insertion et la suppression de lots pour les UITableViews dans le Guide de programmation des vues de tableaux, mais il ne couvre pas spécifiquement ce point.

Je pense que ce qui se passe, c'est que la suppression des sections fait que les suppressions de lignes se réfèrent à la mauvaise ligne.

Par exemple, vous voulez supprimer la section #2 et la ligne #1 de la section #4... mais après avoir supprimé la section #2, l'ancienne section #4 est maintenant la troisième section, donc lorsque vous supprimez avec l'ancien NSIndexPath de (4, 1), vous supprimez une ligne différente aléatoire qui peut ne pas exister.

Je pense donc que la solution pourrait être aussi simple que de permuter ces deux lignes de code, de sorte que vous supprimiez d'abord les lignes, puis les sections.

3voto

Corey Floyd Points 16747

Voici donc enfin ma solution à ce problème. Cette méthode peut être appliquée à des tableaux de n'importe quelle taille et de n'importe quel nombre de sections (pour autant que je sache).

Comme précédemment, j'ai modifié le code de tableview de Matt Gallagher qui place la logique spécifique à la cellule dans un contrôleur de cellule séparé. Cependant, vous pouvez facilement adapter cette méthode à un modèle différent

J'ai ajouté les ivars suivants (pertinents) au code de 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

L'ivar original de Matt :

NSArray *allTableGroups

pointe toujours vers l'un des tableaux ci-dessus.

Cela peut probablement être refactorisé et amélioré de manière significative, mais je n'en ai pas eu besoin. De plus, si vous utilisez Core Data, NSFetchedResultsController rend cela plus facile.

Passons maintenant à la méthode (j'essaie de commenter autant que possible) :

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

}

1voto

bfulgham Points 71

Je soupçonne que vous avez oublié de supprimer l'objet représentant la section de votre stockage interne, de sorte que l'objet de la section ne soit plus disponible. -numberOfSectionsInTableView: renvoie toujours 1 après que toutes les sections ont été supprimées.

C'est exactement ce que je faisais mal quand j'ai eu le même accident !

1voto

TRK3 Points 11

J'ai vu cette même erreur exacte comme le résultat de la libération prématurée de la vue d'arrière-plan de ma cellule de tableau personnalisée.

Avec NSZombieEnabled, j'ai obtenu une exception lancée bien en dessous d'un appel interne à une fonction pour préparer la cellule à la réutilisation. Sans NSZombieEnabled, j'obtenais l'erreur de cohérence interne.

Par ailleurs, lorsque j'ai corrigé le problème de conservation/libération sur la vue d'arrière-plan de la cellule, j'ai pu supprimer la dernière ligne de la section sans avoir à supprimer explicitement la section.

Morale de l'histoire : Cette erreur signifie simplement que quelque chose de grave se produit lorsque vous essayez de supprimer, et l'une des choses qui se produit lorsque vous supprimez est que la cellule est préparée pour être réutilisée, donc si vous faites quelque chose de personnalisé avec les cellules de votre tableau, recherchez une erreur possible.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X