CollectionView + UIKit Dynamics在performBatchUpdates时崩溃

7

我遇到了一个奇怪的崩溃,一整天都在尝试修复它。 我有一个自定义的UICollectionViewLayout,基本上为单元格添加重力和碰撞行为。

这个实现非常好! 问题出现在我尝试使用 [self.collectionView performBatchUpdates:] 删除一个单元格时。

它给了我以下错误:

2013-12-12 21:15:35.269 APPNAME[97890:70b] *** Assertion failure in -[UICollectionViewData validateLayoutInRect:], /SourceCache/UIKit_Sim/UIKit-2935.58/UICollectionViewData.m:357

2013-12-12 20:55:49.739 APPNAME[97438:70b] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UICollectionView recieved layout attributes for a cell with an index path that does not exist: <NSIndexPath: 0x975d290> {length = 2, path = 0 - 4}'

我的模型被正确处理了,我可以看到它正在从中删除该项!
删除项目的indexPath已经在对象之间正确传递。 唯一不会崩溃的情况是当我删除它上面的最后一个单元格时,否则就会崩溃。
这是我用来删除单元格的代码。
- (void)removeItemAtIndexPath:(NSIndexPath *)itemToRemove completion:(void (^)(void))completion
{
    UICollectionViewLayoutAttributes *attributes = [self.dynamicAnimator layoutAttributesForCellAtIndexPath:itemToRemove];

    [self.gravityBehaviour removeItem:attributes];
    [self.itemBehaviour removeItem:attributes];
    [self.collisionBehaviour removeItem:attributes];

    [self.collectionView performBatchUpdates:^{
        [self.fetchedBeacons removeObjectAtIndex:itemToRemove.row];
        [self.collectionView deleteItemsAtIndexPaths:@[itemToRemove]];
    } completion:nil];   
}

处理单元格属性的CollectionView委托基本如下。

- (CGSize)collectionViewContentSize
{
    return self.collectionView.bounds.size;
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    return [self.dynamicAnimator itemsInRect:rect];
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    return [self.dynamicAnimator layoutAttributesForCellAtIndexPath:indexPath];
}

我已经尝试过以下方法,但都没有成功: - 使布局失效 - 重新加载数据 - 从UIDynamicAnimator中删除行为并在更新后再次添加

有什么见解吗?

这个问题的源代码可以在此存储库中找到。请查看。 代码存储库

最好的祝愿。 乔治。


谢谢David。这是加载、删除和崩溃序列的输出结果。看起来计数没问题。输出日志链接 - George Villasboas
你能在performBatchUpdates之前和之后添加日志,并在每个代码行的块之前、中间和之后添加日志吗?还有,请在删除后(将其作为中间日志消息)记录self.fetchedBeacons中的项目数量,然后更新日志。我知道这很麻烦,但是我在处理简历时也是这样做的,我编写了一个非常简单的单视图应用程序,在单元格中使用标签来尝试不同的操作模式。如果那个方法有效而复杂的代码无效,那就是CV出错了(最有可能)。似乎CV会根据单元格的复杂程度改变行为,这对我们来说真的很困难! - David H
@DavidH 谢谢你,David。我已经尝试了建议的链接。重新加载数据和使布局无效似乎是无用的。看起来dynamicAnimator完全控制了它。此外,我正在使用自定义布局,因此此处永远不会调用标题大小。我在一个工作示例中隔离了代码。如果你有时间并想深入研究这个错误,请查看它。代码存储库最好!!! - George Villasboas
你说你的计数没问题,但如果你直接在集合视图上执行删除操作——而不是使用perform batch updates,你会发现有不匹配的情况。当我运行你的代码并将删除操作移出perform batch调用时,我得到了这样的错误提示:“更新后现有部分中包含的项目数(5)必须等于更新前该部分中包含的项目数(5)加上或减去从该部分中插入或删除的项目数(0插入,1删除),以及加上或减去移入或移出该部分的项目数(0移入,0移出)。”。 - Matt Long
如果你还没有更新你的Github项目,我真心希望你能够更新,因为这是一个非常棒的演示! - David H
显示剩余3条评论
4个回答

12

在苦苦挣扎了几周后,从我们的朋友 @david-h 和 @erwin 得到了一些有用的见解,还收到了 Apple 的 WWDR 的电话和电子邮件,我终于能够解决这个问题。只是想与社区分享解决方案。

  1. 问题。UIKit Dynamics 有一些帮助方法来处理 Collection Views,但这些方法实际上没有执行一些内部操作,我认为它应该自动执行这些操作,例如在执行 performBatchUpdates: 时自动更新动态动画单元格。因此,根据 WWDR 的说法,您必须手动执行此操作,因此我们遍历已更新项并更新其 NSIndexPaths,以便动态动画器可以向我们提供有关单元格的正确更新。

  2. 错误。即使在插入/删除单元格时执行了此 IndexPath 更新操作,我仍然遇到了许多随机崩溃和奇怪的动画行为。所以,我遵循了 @erwin 给出的提示,即在 performBatchUpdates: 后重新实例化 UIDynamicAnimator,并解决了这种情况下的所有问题。

所以代码如下:

- (void)removeItemAtIndexPath:(NSIndexPath *)itemToRemove completion:(void (^)(void))completion
{
    UICollectionViewLayoutAttributes *attributes = [self.dynamicAnimator layoutAttributesForCellAtIndexPath:itemToRemove];

    if (attributes) {
        [self.collisionBehaviour removeItem:attributes];
        [self.gravityBehaviour removeItem:attributes];
        [self.itemBehaviour removeItem:attributes];

        // Fix the problem explained on 1.
        // Update all the indexPaths of the remaining cells
        NSArray *remainingAttributes = self.collisionBehaviour.items;
        for (UICollectionViewLayoutAttributes *attributes in remainingAttributes) {
            if (attributes.indexPath.row > itemToRemove.row)
                attributes.indexPath = [NSIndexPath indexPathForRow:(attributes.indexPath.row - 1) inSection:attributes.indexPath.section];
        }

        [self.collectionView performBatchUpdates:^{
            completion();
            [self.collectionView deleteItemsAtIndexPaths:@[itemToRemove]];
        } completion:nil];

        // Fix the bug explained on 2.
        // Re-instantiate the Dynamic Animator
        self.dynamicAnimator = [[UIDynamicAnimator alloc] initWithCollectionViewLayout:self];
        [self.dynamicAnimator addBehavior:self.collisionBehaviour];
        [self.dynamicAnimator addBehavior:self.gravityBehaviour];
        [self.dynamicAnimator addBehavior:self.itemBehaviour];
    }
}

我打开了一个Radar来解释这个问题,希望苹果公司在未来的更新中修复它。如果有人想要复制它,它可以在OpenRadar上找到。

谢谢大家。


你测试过在iOS 7.1中是否仍存在这个问题吗? - Steffen Andersen

4
我遇到了类似的情况。
在我的情况下,我使用UIAttachmentBehaviors,所以每个UICollectionViewLayoutAttributes项都有自己的行为。因此,我不是从行为中删除项目,而是从动态动画师中删除相应的行为。
对于我来说,删除一个中间的UICollectionViewCell似乎可以工作(没有崩溃),但如果我尝试删除最后一个单元格,则应用程序将崩溃。
仔细检查动画师的行为(使用调试日志)显示剩余项目的索引路径确实偏离删除的项目一个。仅手动重置它们并不能解决问题。
问题似乎在于集合视图中的单元格数量与动态动画师的-itemsInRect:返回的项目数不匹配(我的所有单元格始终可见)。
我可以通过在删除单元格之前删除所有行为来防止崩溃。但是,当附件消失时,这当然会导致我的单元格出现不想要的移动。
我真正需要的是一种在不完全丢弃它们并重新创建它们的情况下重置动态动画师中的项目的方法。
因此,最终,我想出了一种基于将行为存储到侧面的机制,重新实例化动态动画师,并重新添加行为。
它似乎工作得很好,并且可能可以进一步优化。
- (void)detachItemAtIndexPath:(NSIndexPath *)indexPath completion:(void (^)(void))completion {

for (UIAttachmentBehavior *behavior in dynamicAnimator.behaviors) {
    UICollectionViewLayoutAttributes *thisItem = [[behavior items] firstObject];
    if (thisItem.indexPath.row == indexPath.row) {
        [dynamicAnimator removeBehavior:behavior];
    }
    if (thisItem.indexPath.row > indexPath.row) {
        thisItem.indexPath = [NSIndexPath indexPathForRow:thisItem.indexPath.row-1 inSection:0];
    }
}

NSArray *tmp = [NSArray arrayWithArray:dynamicAnimator.behaviors];

self.dynamicAnimator = [[UIDynamicAnimator alloc] initWithCollectionViewLayout:self];

for (UIAttachmentBehavior *behavior in tmp) {
    [dynamicAnimator addBehavior:behavior];
}

    // custom algorithm to place cells
for (UIAttachmentBehavior *behavior in dynamicAnimator.behaviors) {
    [self setAnchorPoint:behavior];
}

[self.collectionView performBatchUpdates:^{
    [self.collectionView deleteItemsAtIndexPaths:@[indexPath]];
} completion:^(BOOL finished) {
            completion();
}];

}


2

首先,这是一个惊人的项目!我喜欢盒子弹跳的效果。似乎总有一个缺失-第0行。无论如何,任何对布局感兴趣的读者都应该看看它!动画很棒。

在...Layout.m中:

将此方法更改为重新加载:

- (void)removeItemAtIndexPath:(NSIndexPath *)itemToRemove completion:(void (^)(void))completion
{
    //assert([NSThread isMainThread]);

    UICollectionViewLayoutAttributes *attributes = [self.dynamicAnimator layoutAttributesForCellAtIndexPath:itemToRemove];
    [self.collisionBehaviour removeItem:attributes];
    [self.gravityBehaviour removeItem:attributes];
    [self.itemBehaviour removeItem:attributes];

    completion();

    [self.collectionView reloadData];
}

我添加了以下日志信息:

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSLog(@"ASKED FOR LAYOUT ELEMENTS IN RECT");
    NSArray *foo = [self.dynamicAnimator itemsInRect:rect];
    NSLog(@"...LAYOUT ELEMENTS %@", foo);
    return foo;
}

运行程序并删除中间的一个项目。查看控制台中的索引路径。当您删除一个项目时,必须重置索引路径,因为它们现在不正确地反映了单元格的新索引。

CollectionViewDynamics[95862:70b] ASKED FOR LAYOUT ELEMENTS IN RECT
CollectionViewDynamics[95862:70b] ...LAYOUT ELEMENTS (
    "<UICollectionViewLayoutAttributes: 0x109405530> index path: (<NSIndexPath: 0xc000000000008016> {length = 2, path = 0 - 1}); frame = (105.41 -102.09; 100.18 100.18); transform = [0.99999838000043739, -0.0017999990280001574, 0.0017999990280001574, 0.99999838000043739, 0, 0]; ",
    "<UICollectionViewLayoutAttributes: 0x10939c2b0> index path: (<NSIndexPath: 0xc000000000018016> {length = 2, path = 0 - 3}); frame = (1 -100.5; 100 100); ",
    "<UICollectionViewLayoutAttributes: 0x10912b200> index path: (<NSIndexPath: 0xc000000000000016> {length = 2, path = 0 - 0}); frame = (3 468; 100 100); "
)
CollectionViewDynamics[95862:70b] *** Assertion failure in -[UICollectionViewData validateLayoutInRect:], /SourceCache/UIKit_Sim/UIKit-2935.80.1/UICollectionViewData.m:357

修复这个问题应该可以让你继续前进(希望如此)。NSLog多年来一直是我的好朋友!可能因人而异。


嗨@david-h!非常感谢。很高兴你喜欢这个动画。最终效果更加有趣。让我们保持联系,这样我就可以在准备好时向你展示。但是,这里仍然没有成功。更新索引路径应该由重新加载CV处理,手动操作会引入许多其他奇怪的错误。由于我有点厌倦填写Radars / Bugreports并且从未得到反馈,所以我使用了我的一个帐户支持请求。让我们看看苹果工程师能否解决这个问题。如果有答案,我将非常乐意发布最终答案。谢谢,保重! - George Villasboas

0

通过一行代码解决了类似的问题

self.<#custom_layout_class_name#>.dynamicAnimator = nil;

每次更新数据源都必须进行类型转换


这是我解决问题的第一次尝试之一,但它引发了与集合视图相关的另一个崩溃。 - George Villasboas

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