【问题标题】:UITableView: deleting sections with animationUITableView:删除带有动画的部分
【发布时间】:2010-11-06 20:37:50
【问题描述】:

更新

我已经在下面发布了我对这个问题的解决方案作为答案。它采用了与我的第一次修订不同的方法。


原始问题 我之前问过一个我认为解决了我的问题的问题:

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

但是,当我从 UITableView 中删除部分时,我现在又遇到了类似的问题。 (当我改变表格中的部分/行数时,它们重新出现)。

在因为我的帖子太长而失去你之前,让我清楚地说明问题,你可以阅读尽可能多的内容来提供答案。


问题:

如果从 UITableView 批量删除行和部分,应用程序有时会崩溃。这取决于表格的配置以及我选择删除的行和部分的组合。

日志说我崩溃了,因为它说我没有正确更新数据源和表:

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

现在很快,在你写出明显的答案之前,我向你保证,我确实已经从数据源中正确地添加和删除了行和部分。解释很长,但你会在下面找到它,按照方法。

那么,如果你仍然感兴趣的话……


处理删除部分和行的方法:

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


}

我的猜测:

问题似乎是这样的:如果您查看上面我列出每个部分中的单元格数量的位置,您会看到第 5 部分似乎增加了 1。但是,事实并非如此。原来的第 5 节实际上已被删除,取而代之的是另一个节(具体来说,它是旧的第 10 节)。

那么为什么表格视图似乎没有意识到这一点?它应该知道我删除了旧部分并且不应该期望现在位于旧部分索引处的新部分受已删除部分的行数的约束。

希望这是有道理的,写出来有点复杂。

(请注意,此代码之前使用不同数量的行/部分。此特定配置似乎给它带来了问题)

【问题讨论】:

    标签: iphone cocoa-touch uitableview uikit


    【解决方案1】:

    我以前遇到过这个问题。您正在尝试从一个部分中删除所有行,然后,此外,该部分现在为空。但是,仅删除该部分就足够(并且适当)了。其中的所有行也将被删除。这是我的项目中处理删除一行的一些示例代码。它需要确定是否应该从一个节中只删除这一行,或者如果它是该节中的最后一行,则删除整个节:

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

    【讨论】:

      【解决方案2】:

      我注意到您先从表中删除部分,然后再删除行。

      我知道 Table View Programming Guide 中有一个用于 UITableViews 的 complicated discussion of batch insertion and deletion,但它并没有具体涵盖这一点。

      我认为正在发生的事情是删除部分会导致行删除引用错误的行。

      即您想从第 4 节中删除第 2 节和第 1 行...但是在删除第 2 节之后,旧的第 4 节现在是第三节,所以当您使用旧的 NSIndexPath 删除时( 4, 1) 您正在删除一些可能不存在的随机不同行。

      所以我认为修复可能就像交换这两行代码一样简单,因此您首先删除行,然后删除部分。

      【讨论】:

      • 或者,跟踪您需要删除的每个单元格的 indexPath,并在删除时适当地调整它们。 (这可能是一种冗长/复杂/不恰当的方式——只是一个想法。)
      • 我正在进行批量删除,所以我列出操作的顺序没有区别。表视图在更新块内时“立即”执行操作。由于我是偏执狂,我确实尝试切换操作顺序无济于事。在批量删除期间,部分/行的编号不会切换(不应切换)。如果没有使用积木,那你就对了。
      • @Tim 有趣的想法。你是对的,大量删除(我会删除)可能会非常乏味。我也想知道我是否可以快速连续进行多次删除。我试图进行批量删除以避免这些问题,但这可能是必要的。
      • 我想我的下一步是尝试考虑哪些表配置会导致崩溃而不是。此外,验证过滤后的表格是否显示正确的行,这样您就不会删除您不期望的内容...尝试一些简单的情况,例如删除一个完整的部分以及该部分之前或之后的一行,并确保您期望的行消失了...
      • 如果您查看我的 NSLogs,我确实打印出了相关信息,显示了它引发异常的确切原因。我已经测试了其他有效的案例,但是这个特定的案例打破了。我试图找出为什么这个案例会破裂而其他案例却没有。
      【解决方案3】:

      所以最后这是我对这个问题的解决方案。 这种方法可以应用于任何大小、任何节数的表格(据我所知)

      和以前一样,我修改了 Matt Gallagher 的 tableview 代码,它将单元特定的逻辑放在单独的单元控制器中。但是,您可以轻松地将这种方法应用于不同的模型

      我在 Matt 的代码中添加了以下(相关的)ivars:

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

      Matt 的原 ivar:

      NSArray *allTableGroups
      

      ...总是指向上述数组之一。

      这可能可以重构和显着改进,但我没有需要。此外,如果您使用 Core Data,NSFetchedResultsController 会使这更容易。

      现在谈谈方法(我正在尽可能多地发表评论):

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

      【讨论】:

        【解决方案4】:

        由于过早释放自定义表格视图单元格的背景视图,我看到了同样的错误。

        使用 NSZombieEnabled 时,我在内部调用函数下方抛出异常,以准备单元以供重用。如果没有 NSZombieEnabled,我会收到内部一致性错误。

        顺便说一句,当我在单元格的背景视图上修复保留/释放问题时,我能够删除该部分的最后一行,而无需显式删除该部分。

        故事的寓意:这个错误只是意味着当你尝试删除时发生了一些不好的事情,当你删除时发生的事情之一是单元格准备好重复使用,所以如果你对你的 tableview 做任何自定义单元格,在那里寻找可能的错误。

        【讨论】:

          【解决方案5】:

          我怀疑您忘记从内部存储中删除代表该部分的对象,因此在删除所有部分后-numberOfSectionsInTableView: 方法仍然返回 1。

          当我遇到同样的崩溃时,这正是我做错的!

          【讨论】:

          • 引用问题:现在,在你写出明显的答案之前,我向你保证,我确实已经从数据源中正确添加和删除了行和部分。
          【解决方案6】:

          解决这个问题的一个更简单的方法是更新您的数据源,然后调用reloadSections

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

          这将重新加载单个部分。或者,您可以使用indexSetWithIndexesInRange: 同时重新加载多个部分。

          【讨论】:

            【解决方案7】:

            或者干脆这样做

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

            文件夹的目录是你的数组!这就是上面所有的代码对我不起作用!这样做成本更低,而且很有意义!

            【讨论】: