web-dev-qa-db-ja.com

UITableView:アニメーションを使用したセクションの削除

更新

この問題の解決策を以下の回答として投稿しました。それは私の最初のリビジョンとは異なるアプローチを取ります。


元の質問以前にSOで問題を解決したと思う質問をしました:

行の削除中に非表示の行を処理する方法。(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).

すぐに、明白な答えを書く前に、dataSourceに行とセクションを適切に追加および削除したことを確認します。説明は長くなりますが、以下の方法に従って説明します。

それで、まだ興味があるなら…


セクションと行の削除を処理する方法:

- (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です)。

では、なぜテーブルビューはこれを実現していないように見えるのでしょうか?古いセクションを削除したことを知っている必要があり、古いセクションのインデックスにある新しいセクションが削除されたセクションの行数によってバインドされることを期待しないでください。

うまくいけば、これが理にかなっており、これを書くのは少し複雑です。

(このコードは以前は異なる数の行/セクションで機能していました。この特定の構成では問題が発生するようです)

40
Corey Floyd

以前にこの問題に遭遇しました。セクションからすべての行を削除し、さらにその空のセクションを削除しようとしています。ただし、そのセクションのみを削除すれば十分です(そして適切です)。その中のすべての行も削除されます。 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];
    }
}
88
Martin Winter

最初にテーブルからセクションを削除し、次に行を削除していることに気付きました。

Table View Programming GuideのUITableViewsには バッチの挿入と削除の複雑な議論 がありますが、これについては特に取り上げていません。

何が起こっているのかと言えば、セクションを削除すると、行の削除が間違った行を参照することになります。

つまり、セクション#4からセクション#2と行#1を削除しますが、セクション#2を削除すると、古いセクション#4が3番目のセクションになるため、古いNSIndexPathで削除すると(4、1)存在しない可能性のあるランダムな異なる行を削除しています。

したがって、修正はこれらの2行のコードを交換するのと同じくらい簡単かもしれないので、最初に行を削除し、次にセクションを削除します。

4
David Maymudes

最後に、この問題に対する私の解決策を示します。このメソッドは、任意のサイズ、任意の数のセクションのテーブルに適用できます(私が知る限り)

前と同じように、セル固有のロジックを別のセルコントローラに配置するMatt Gallagherのtableviewコードを変更しました。ただし、この方法を別のモデルに簡単に適合させることができます

次の(関連する)ivarを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:

NSArray *allTableGroups

…常に上記の配列のいずれかを指します。

これはおそらくリファクタリングして大幅に改善できますが、必要はありませんでした。また、コアデータを使用する場合、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];


}
3
Corey Floyd

これに対処するはるかに簡単な方法は、データソースを更新してからreloadSectionsを呼び出すことです。

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

これにより、単一のセクションがリロードされます。または、indexSetWithIndexesInRange:を使用して、複数のセクションを同時にリロードできます。

1
Kyle Clegg

セクションを表すオブジェクトを内部ストレージから削除するのを忘れていると思われるので、-numberOfSectionsInTableView:メソッドは、すべてのセクションが削除された後も1を返します。

それはまさに同じクラッシュがあったときに私が間違っていたことでした!

1
bfulgham

カスタムテーブルビューセルの背景ビューを時期尚早に解放した結果、これとまったく同じエラーが発生しました。

NSZombieEnabledを使用すると、セルを再利用できるように準備するための関数の内部呼び出しの下に例外がスローされます。 NSZombieEnabledがなければ、内部整合性エラーが発生していました。

ちなみに、セルの背景ビューで保持/解放の問題を修正したとき、セクションを明示的に削除することなく、セクションの最後の行を削除できました。

ストーリーの教訓:このエラーは、削除しようとしたときに何か悪いことが起こっていることを意味します。エラーの可能性があります。

1
TRK3

または単にこれを行う

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

あなたの配列であるフォルダーのディレクトリ!それは上記のすべてのコードが私にとってうまくいかなかったことです!これは安価であり、理にかなっています!

0
FreeAppl3