使用performBatchUpdates崩溃的梦魇

时间:2016-06-15 22:10:45

标签: ios iphone uicollectionview performbatchupdates

我在performBatchUpdates collection view期间遇到了一场噩梦。

问题基本上是这样的:我在服务器上的目录上有很多图像。我想在collection view上显示这些文件的缩略图。但缩略图必须从服务器异步下载。当它们到达时,它们将使用以下内容插入到集合视图中:

dispatch_async(dispatch_get_main_queue(),
             ^{
               [self.collectionView performBatchUpdates:^{

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

                 if (changedIndexes && [changedIndexes count] > 0) {
                   [self.collectionView reloadItemsAtIndexPaths:changedIndexes];
                 }

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

               } completion:nil];
             });

问题是这个(我想)。假设在time = 0,集合视图有10个项目。然后我再向服务器添加100个文件。应用程序查看新文件并开始下载缩略图。随着缩略图下载,它们将被插入到集合视图中。但是因为下载可能需要不同的时间,并且这个下载操作是asynchronous,所以在某一时刻,iOS将无法跟踪该集合有多少元素,并且这一灾难性的臭名昭着的消息将导致整个事件崩溃。

  

***由于未捕获的异常终止应用' NSInternalInconsistencyException',原因:'无效更新:无效   第0节中的项目数。包含在项目中的项目数   更新后的现有部分(213)必须等于数量   更新前的该部分中包含的项目(154),加号或减号   从该部分插入或删除的项目数(40   插入,0删除)并加上或减去移入的项目数   或超出该部分(0移入,0移出)。'

我有一些可疑的证据是,如果我在数据集上打印项目数,我会看到213.所以,数据集匹配正确的数字,而且消息是无意义的。

我之前遇到过这个问题,here但这是一个iOS 7项目。不知何故,问题现在在iOS 8上返回,而那里的解决方案无效,现在数据集 IS IN SYNC

4 个答案:

答案 0 :(得分:4)

听起来你需要做一些额外的工作来批量为每个动画group出现哪些图像。从处理此类崩溃之前,performBatchUpdates的工作方式是

  1. 在调用您的块之前,它会仔细检查所有项目计数并通过调用numberOfItemsInSection保存它们(这是您的错误消息中的154)。
  2. 它运行块,跟踪插入/删除,并根据插入和删除计算的最终项目数。
  3. 在块运行后,当它询问您的dataSource numberOfItemsInSection时(它是213号),它会仔细检查计算的计数与实际计数。如果它不匹配,它将崩溃。
  4. 根据您的变量insertedIndexeschangedIndexes,您可以根据服务器的下载响应预先计算需要显示的内容,然后运行批处理。但是我猜你的numberOfItemsInSection方法总是只返回项目的“真实”数。

    因此,如果在步骤2中完成下载,当它在“3”中执行完整性检查时,您的号码将不再排队。

    最简单的解决方案:等到所有文件都已下载,然后执行一次batchUpdates。可能不是最好的用户体验,但它避免了这个问题。

    更难的解决方案:根据需要执行批处理,并跟踪哪些项目已经显示/当前与项目总数分开设置动画。类似的东西:

    BOOL _performingAnimation;
    NSInteger _finalItemCount;
    
    - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
        return _finalItemCount;
    }
    
    - (void)somethingDidFinishDownloading {
        if (_performingAnimation) {
            return;
        }
        // Calculate changes.
        dispatch_async(dispatch_get_main_queue(),
                 ^{
                    _performingAnimation = YES;
                   [self.collectionView performBatchUpdates:^{
    
                     if (removedIndexes && [removedIndexes count] > 0) {
                       [self.collectionView deleteItemsAtIndexPaths:removedIndexes];
                     }
    
                     if (changedIndexes && [changedIndexes count] > 0) {
                       [self.collectionView reloadItemsAtIndexPaths:changedIndexes];
                     }
    
                     if (insertedIndexes && [insertedIndexes count] > 0) {
                       [self.collectionView insertItemsAtIndexPaths:insertedIndexes];
                     }
    
                     _finalItemCount += (insertedIndexes.count - removedIndexes.count);
                   } completion:^{
                     _performingAnimation = NO;
                   }];
                 });
    }
    

    在此之后要解决的唯一问题是确保在动画期间完成下载的最后一项(可能有一个在完成块中运行的方法performFinalAnimationIfNeeded时,对剩余项目进行最后一次检查)

答案 1 :(得分:3)

我认为问题是由索引造成的。

键:

  • 对于更新和删除的项目,索引必须是原始项目的索引。
  • 对于插入的项目,索引必须是最终项目的索引。

以下是带注释的演示代码:

class CollectionViewController: UICollectionViewController {

    var items: [String]!

    let before = ["To Be Deleted 1", "To Be Updated 1", "To Be Updated 2", "To Be Deleted 2", "Stay"]
    let after = ["Updated 1", "Updated 2", "Added 1", "Stay", "Added 2"]

    override func viewDidLoad() {
        super.viewDidLoad()

        self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Refresh", style: .Plain, target: self, action: #selector(CollectionViewController.onRefresh(_:)))

        items = before
    }

    func onRefresh(_: AnyObject) {

        items = after

        collectionView?.performBatchUpdates({
            self.collectionView?.deleteItemsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0), NSIndexPath(forRow: 3, inSection: 0), ])

            // Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete and reload the same index path
            // self.collectionView?.reloadItemsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0), NSIndexPath(forRow: 1, inSection: 0), ])

            // NOTE: Have to be the indexes of original list
            self.collectionView?.reloadItemsAtIndexPaths([NSIndexPath(forRow: 1, inSection: 0), NSIndexPath(forRow: 2, inSection: 0), ])

            // Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to insert item 4 into section 0, but there are only 4 items in section 0 after the update'
            // self.collectionView?.insertItemsAtIndexPaths([NSIndexPath(forRow: 4, inSection: 0), NSIndexPath(forRow: 5, inSection: 0), ])

            // NOTE: Have to be index of final list
            self.collectionView?.insertItemsAtIndexPaths([NSIndexPath(forRow: 2, inSection: 0), NSIndexPath(forRow: 4, inSection: 0), ])

        }, completion: nil)
    }

    override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return 1
    }
    override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return items.count
    }

    override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier("MyCell", forIndexPath: indexPath)

        let label = cell.viewWithTag(100) as! UILabel

        label.text = items[indexPath.row]

        return cell
    }
}

答案 2 :(得分:2)

对于有类似问题的任何人,让我引用UICollectionView上的文档:

  

如果在调用此方法之前集合视图的布局不是最新的,则可能会重新加载。为避免出现问题,应在调用performBatchUpdates(_:completion:)之前在updates块内更新数据模型或确保布局已更新。

我最初引用的是一个单独的模型对象的数组,但是决定在视图控制器中保留该数组的本地副本,并在performBatchUpdates(_:completion:)中更新该数组。

问题解决了。

答案 3 :(得分:0)

这可能是由于您确实需要确保使用collectionViews删除和插入节而发生的。当您尝试在不存在的部分中插入项目时,将会发生此崩溃。

Preform Batch更新不知道您打算在X + 1 X处插入项目时添加X + 1部分,而您尚未在其中添加该部分。