UICollectionView 断言失败

61

UICollectionView中执行insertItemsAtIndexPaths时,我遇到了以下错误:

断言失败:

-[UICollectionViewData indexPathForItemAtGlobalIndex:], 
/SourceCache/UIKit/UIKit-2372/UICollectionViewData.m:442
2012-09-26 18:12:34.432  
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', 
reason: 'request for index path for global index 805306367 
when there are only 1 items in the collection view'

我已经检查过,我的数据源只包含一个元素。 有什么见解,为什么会发生这种情况?如果需要更多信息,我肯定可以提供。

14个回答

68

当我向集合视图插入第一个单元格时,我遇到了同样的问题。通过更改代码,我调用了UICollectionView来解决这个问题。

- (void)reloadData

在插入第一个单元格时使用该方法,但是

- (void)insertItemsAtIndexPaths:(NSArray *)indexPaths

在插入所有其他单元格时。

有趣的是,我也遇到了一个问题,那就是

- (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths

当删除最后一个单元格时,我与之前做的一样:只需在删除最后一个单元格时调用reloadData.


14
这似乎是正确答案。如果您正在插入第一个单元格,则会出现崩溃。这个问题有一个公开的雷达:http://openradar.appspot.com/12954582 ,其中指出问题仅在使用“带装饰视图”(节标题或页脚)时才会出现。 - chris
2
使用补充视图并在第一个项目上使用reloadData,然后在后续项目上使用insertItemsAtIndexPaths对我无效。但是,如果我禁用补充视图,则可以正常工作。如果我只使用reloadData,则可以正常工作。不幸的是,我想要将-insertItemsAtIndexPaths与我的KVO代码一起使用。 - neoneye
@neoneye 想知道这个问题对你来说是否仍然存在?我在批量插入和补充视图共存方面没有遇到任何问题。 - Timothy Moose
2
在iOS7中,除非我使用reloadData进行单元格插入和删除,否则它总是崩溃!!!我有一个复杂的headerView,并且正在使用简单的单元格进行测试-这很好用。当单元格变得更加复杂时,崩溃就开始了... - David H
1
在我的iOS 7.1上无法工作。我没有任何装饰视图,这个错误发生在不同的单元格号码上,而不仅仅是在添加第一个单元格时。 - Ortwin Gentz
显示剩余2条评论

11

在插入单元格之前插入第0个section似乎能让UICollectionView更加稳定。

NSArray *indexPaths = /* indexPaths of the cells to be inserted */
NSUInteger countBeforeInsert = _cells.count;
dispatch_block_t updates = ^{
    if (countBeforeInsert < 1) {
        [self.collectionView insertSections:[NSIndexSet indexSetWithIndex:0]];
    }
    [self.collectionView insertItemsAtIndexPaths:indexPaths];
};
[self.collectionView performBatchUpdates:updates completion:nil];

这对我来说似乎有效,我还为该部分使用了一个补充视图。 - Adam Carter
2
为了让它正常工作,我不得不调整numberOfSectionsInCollectionView:方法的返回值。如果你的集合为空,则返回0个部分,如果集合不为空,则返回1个部分。如果您没有实现此方法或返回0,则在插入后会出现“无效更新”错误,抱怨部分数量不正确。 - nrj
对我来说能够工作。当我尝试插入一个部分的第一个单元格时,我遇到了相同的错误。现在我改为:1、要插入一个部分的第一个单元格,请使用insertSections:而不是insertItemsAtIndexPaths:。2、要重置现有部分的单元格,请使用insertItemsAtIndexPaths:。 - Lei Zhao

5

我在这里提供了解决此问题的方法:https://gist.github.com/iwasrobbed/5528897

在你的.m文件顶部的私有类别中:

@interface MyViewController ()
{
    BOOL shouldReloadCollectionView;
    NSBlockOperation *blockOperation;
}
@end

然后你的代理回调将会是:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
    shouldReloadCollectionView = NO;
    blockOperation = [NSBlockOperation new];
}

- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id<NSFetchedResultsSectionInfo>)sectionInfo
           atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
    __weak UICollectionView *collectionView = self.collectionView;
    switch (type) {
        case NSFetchedResultsChangeInsert: {
            [blockOperation addExecutionBlock:^{
                [collectionView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]];
            }];
            break;
        }

        case NSFetchedResultsChangeDelete: {
            [blockOperation addExecutionBlock:^{
                [collectionView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]];
            }];
            break;
        }

        case NSFetchedResultsChangeUpdate: {
            [blockOperation addExecutionBlock:^{
                [collectionView reloadSections:[NSIndexSet indexSetWithIndex:sectionIndex]];
            }];
            break;
        }

        default:
            break;
    }
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
    __weak UICollectionView *collectionView = self.collectionView;
    switch (type) {
        case NSFetchedResultsChangeInsert: {
            if ([self.collectionView numberOfSections] > 0) {
                if ([self.collectionView numberOfItemsInSection:indexPath.section] == 0) {
                    shouldReloadCollectionView = YES;
                } else {
                    [blockOperation addExecutionBlock:^{
                        [collectionView insertItemsAtIndexPaths:@[newIndexPath]];
                    }];
                }
            } else {
                shouldReloadCollectionView = YES;
            }
            break;
        }

        case NSFetchedResultsChangeDelete: {
            if ([self.collectionView numberOfItemsInSection:indexPath.section] == 1) {
                shouldReloadCollectionView = YES;
            } else {
                [blockOperation addExecutionBlock:^{
                    [collectionView deleteItemsAtIndexPaths:@[indexPath]];
                }];
            }
            break;
        }

        case NSFetchedResultsChangeUpdate: {
            [blockOperation addExecutionBlock:^{
                [collectionView reloadItemsAtIndexPaths:@[indexPath]];
            }];
            break;
        }

        case NSFetchedResultsChangeMove: {
            [blockOperation addExecutionBlock:^{
                [collectionView moveItemAtIndexPath:indexPath toIndexPath:newIndexPath];
            }];
            break;
        }

        default:
            break;
    }
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    // Checks if we should reload the collection view to fix a bug @ http://openradar.appspot.com/12954582
    if (shouldReloadCollectionView) {
        [self.collectionView reloadData];
    } else {
        [self.collectionView performBatchUpdates:^{
            [blockOperation start];
        } completion:nil];
    }
}

这种方法的功劳归功于Blake Watters。

1
我在这里找到了Blake的方法:https://gist.github.com/Lucien/4440c1cba83318e276bb,但由于某个错误(我认为是这样),我无法使其正常工作。非常感谢您抽出时间发布这个完整版本。它运行得非常好! - PJC
谢谢。看起来NSFetchedResultsChangeInsert中的if子句应该检查*new*IndexPath.section部分中的项目数量。 - Simon Whitaker
使用NSBlockOperation这样的方式是危险的,因为它不能保证添加的块将在主线程上调用。有关详细信息,请参见我的评论此处 - Matej Bukovinski

4
这里有一个非黑客入侵的、基于文档的解决方案。在我的情况下,根据条件我会从collectionView:viewForSupplementaryElementOfKind:atIndexPath:返回一个有效的或空的补充视图。在遇到崩溃后,我检查了文档,以下是文档中的说明:

这个方法必须始终返回一个有效的视图对象。如果您不想在特定情况下使用补充视图,则布局对象不应为该视图创建属性。或者,您可以通过将相应属性的hidden属性设置为YES或将属性的alpha属性设置为0来隐藏视图。要在流式布局中隐藏头部和底部视图,还可以将这些视图的宽度和高度设置为0。

虽然还有其他方法,但最快的方法似乎是:

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewFlowLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section {
    return <condition> ? collectionViewLayout.headerReferenceSize : CGSizeZero;
}

这应该是正确的答案,而且这不是苹果的错误! - scorpiozj
我甚至不调用这个方法。 - Paweł Kanarek

3

我的集合视图从两个数据源获取项目,更新它们会导致这个问题。我的解决方法是将数据更新和集合视图重新加载排队:

[[NSOperationQueue mainQueue] addOperationWithBlock:^{

                //Update Data Array
                weakSelf.dataProfile = [results lastObject]; 

                //Reload CollectionView
                [weakSelf.collectionView reloadItemsAtIndexPaths:@[[NSIndexPath indexPathForItem:0 inSection:0]]];
 }];

1
在我的情况下,问题出现在我创建NSIndexPath的方式上。例如,要删除第三个单元格,而不是执行以下操作:
NSIndexPath* indexPath = [NSIndexPath indexPathWithIndex:2];
[_collectionView deleteItemsAtIndexPaths:@[indexPath]];

我需要做的是:
NSIndexPath* indexPath = [NSIndexPath indexPathForItem:2 inSection:0];
[_collectionView deleteItemsAtIndexPaths:@[indexPath]];

1

请确保在numberOfSectionsInCollectionView:方法中返回正确的值。

我用来计算段落的值是空的,因此有0个段落。这导致了异常。

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
    NSInteger sectionCount = self.objectThatShouldHaveAValueButIsActuallyNil.sectionCount;

    // section count is wrong!

    return sectionCount;
}

1

请检查在UICollectionViewDataSource方法中是否返回了正确数量的元素:

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView

是的,它只返回1。显然,这个问题只会在插入第一项时发生。之后就不再是问题了。但是,当我尝试在第一项上只使用“reloadData”集合视图时,就没有问题了。 - jajo87

1
似乎当您要么插入或移动一个单元格到某个包含补充头或脚视图的部分(使用 UICollectionViewFlowLayout 或从其派生的布局),而且在插入/移动之前该部分有0个单元格时,就会发生问题。
通过在包含补充头视图的部分中添加空且不可见的单元格,可以绕过崩溃并仍保持动画效果。示例如下:
  1. 使 - (NSInteger)collectionView:(UICollectionView *)view numberOfItemsInSection:(NSInteger)section 返回该区域实际单元格数量加 1,其中包括标题视图。
  2. - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath 中返回:

    if ((indexPath.section == YOUR_SECTION_WITH_THE_HEADER_VIEW) && (indexPath.item == [self collectionView:collectionView numberOfItemsInSection:indexPath.section] - 1)) {
            cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"EmptyCell" forIndexPath:indexPath];
    }
    

    ... 用于该位置的空单元格。请记得在 viewDidLoad 或任何初始化 UICollectionView 的地方注册单元格重用:

    [self.collectionView registerClass:[UICollectionReusableView class] forCellWithReuseIdentifier:@"EmptyCell"];
    
  3. 使用 moveItemAtIndexPath:insertItemsAtIndexPaths: 而不崩溃。

1
我遇到了这个问题,没有任何补充的视图。 - kelin

1

这是我在项目中使用的解决此错误的方法,我想在这里发布,以防有人觉得有价值。

@interface FetchedResultsViewController ()

@property (nonatomic) NSMutableIndexSet *sectionsToAdd;
@property (nonatomic) NSMutableIndexSet *sectionsToDelete;

@property (nonatomic) NSMutableArray *indexPathsToAdd;
@property (nonatomic) NSMutableArray *indexPathsToDelete;
@property (nonatomic) NSMutableArray *indexPathsToUpdate;

@end

#pragma mark - NSFetchedResultsControllerDelegate


- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
    [self resetFetchedResultControllerChanges];
}


- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
           atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
    switch(type)
    {
        case NSFetchedResultsChangeInsert:
            [self.sectionsToAdd addIndex:sectionIndex];
            break;

        case NSFetchedResultsChangeDelete:
            [self.sectionsToDelete addIndex:sectionIndex];
            break;
    }
}


- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath
{
    switch(type)
    {
        case NSFetchedResultsChangeInsert:
            [self.indexPathsToAdd addObject:newIndexPath];
            break;

        case NSFetchedResultsChangeDelete:
            [self.indexPathsToDelete addObject:indexPath];
            break;

        case NSFetchedResultsChangeUpdate:
            [self.indexPathsToUpdate addObject:indexPath];
            break;

        case NSFetchedResultsChangeMove:
            [self.indexPathsToAdd addObject:newIndexPath];
            [self.indexPathsToDelete addObject:indexPath];
            break;
    }
}


- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    if (self.sectionsToAdd.count > 0 || self.sectionsToDelete.count > 0 || self.indexPathsToAdd.count > 0 || self.indexPathsToDelete > 0 || self.indexPathsToUpdate > 0)
    {
        if ([self shouldReloadCollectionViewFromChangedContent])
        {
            [self.collectionView reloadData];

            [self resetFetchedResultControllerChanges];
        }
        else
        {
            [self.collectionView performBatchUpdates:^{

                if (self.sectionsToAdd.count > 0)
                {
                    [self.collectionView insertSections:self.sectionsToAdd];
                }

                if (self.sectionsToDelete.count > 0)
                {
                    [self.collectionView deleteSections:self.sectionsToDelete];
                }

                if (self.indexPathsToAdd.count > 0)
                {
                    [self.collectionView insertItemsAtIndexPaths:self.indexPathsToAdd];
                }

                if (self.indexPathsToDelete.count > 0)
                {
                    [self.collectionView deleteItemsAtIndexPaths:self.indexPathsToDelete];
                }

                for (NSIndexPath *indexPath in self.indexPathsToUpdate)
                {
                    [self configureCell:[self.collectionView cellForItemAtIndexPath:indexPath]
                            atIndexPath:indexPath];
                }

            } completion:^(BOOL finished) {
                [self resetFetchedResultControllerChanges];
            }];
        }
    }
}

// This is to prevent a bug in UICollectionView from occurring.
// The bug presents itself when inserting the first object or deleting the last object in a collection view.
// https://dev59.com/Smcs5IYBdhLWcg3wu2jS
// This code should be removed once the bug has been fixed, it is tracked in OpenRadar
// http://openradar.appspot.com/12954582
- (BOOL)shouldReloadCollectionViewFromChangedContent
{
    NSInteger totalNumberOfIndexPaths = 0;
    for (NSInteger i = 0; i < self.collectionView.numberOfSections; i++)
    {
        totalNumberOfIndexPaths += [self.collectionView numberOfItemsInSection:i];
    }

    NSInteger numberOfItemsAfterUpdates = totalNumberOfIndexPaths;
    numberOfItemsAfterUpdates += self.indexPathsToAdd.count;
    numberOfItemsAfterUpdates -= self.indexPathsToDelete.count;

    BOOL shouldReload = NO;
    if (numberOfItemsAfterUpdates == 0 && totalNumberOfIndexPaths == 1)
    {
        shouldReload = YES;
    }

    if (numberOfItemsAfterUpdates == 1 && totalNumberOfIndexPaths == 0)
    {
        shouldReload = YES;
    }

    return shouldReload;
}

- (void)resetFetchedResultControllerChanges
{
    [self.sectionsToAdd removeAllIndexes];
    [self.sectionsToDelete removeAllIndexes];
    [self.indexPathsToAdd removeAllObjects];
    [self.indexPathsToDelete removeAllObjects];
    [self.indexPathsToUpdate removeAllObjects];
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接