在滚动和重新加载时,UICollectionViewFlowLayout子类崩溃访问超出边界的数组

时间:2014-07-07 17:57:43

标签: ios uicollectionview uicollectionviewlayout

我有一个UICollectionView,它使用UICollectionViewFlowLayout的自定义子类,使部分标题保持在屏幕顶部,同时像UITableView的简单样式一样滚动。我的代码基于这种方法:

http://blog.radi.ws/post/32905838158/sticky-headers-for-uicollectionview-using

此外,集合视图设置为在用户滚动到底部时加载更多结果。也就是说,当用户到达集合视图的底部时,Web请求会加载更多数据,然后重新加载集合视图,即调用reloadData。

似乎运行良好除了我通过运行iOS 7和7.1的TestFlight从beta测试人员那里得到一些崩溃报告,他们说当他们向下滚动到底部时偶尔发生以下情况(即触发更多结果加载)很快:

*** -[__NSArrayM objectAtIndex:]: index 28 beyond bounds [0 .. 16]
PRIMARY THREAD THREAD 0

__exceptionPreprocess
objc_exception_throw
-[__NSArrayM objectAtIndex:]
-[UICollectionViewFlowLayout(Internal) _frameForItemAtSection:andRow:usingData:]
-[UICollectionViewFlowLayout layoutAttributesForItemAtIndexPath:usingData:]
-[UICollectionViewFlowLayout layoutAttributesForItemAtIndexPath:]
-[MyCustomCollectionViewFlowLayout layoutAttributesForItemAtIndexPath:]
-[MyCustomCollectionViewFlowLayout layoutAttributesForSupplementaryViewOfKind:atIndexPath:]
-[MyCustomCollectionViewFlowLayout layoutAttributesForElementsInRect:]
-[UICollectionViewData validateLayoutInRect:]_block_invoke
-[UICollectionViewData validateLayoutInRect:]
-[UICollectionView layoutSubviews]
-[UIView(CALayerDelegate) layoutSublayersOfLayer:]
-[CALayer layoutSublayers]

似乎当我的自定义流布局代码调用[self.collectionView numberOfItemsInSection:someSection]来获取某个部分的最后一个项目的布局属性时,该调用将根据新加载的数据返回(例如,在这种情况下,一个部分现在具有29个项目)但默认流程布局的内部仍然使用某种缓存数据(例如,在这种情况下,该部分只有17个项目)。不幸的是,我无法自己重现崩溃,即使是经历过它的beta测试人员也无法一致地重现它。

有什么想法吗?

4 个答案:

答案 0 :(得分:6)

编辑2 ,根据BenRB的第2条评论。

当dataSource更新并调用reloadData时,后者确实会使CV中的所有内容无效。 但是,启动的刷新过程的逻辑和确切序列发生在默认流布局中,并且对我们隐藏。

特别是,默认流布局有自己的私有_prepareLayout(确切地说,带有下划线)方法,该方法独立于prepareLayout及其子类提供的重载。

顺便说一句,

prepareLayout(没有下划线)基本流布局类的默认实现什么都不做。

在刷新过程中,默认流布局为其子类提供了通过layoutAttributesForElementsInRect:layoutAttributesForItemAtIndexPath:"回调"提供更多信息(例如,附加layoutAttributes)的机会。为了保证基类与#39;之间的一致性数据和各自的indexPath / layoutAttributes数组,调用相应的" super"应该只在这些相应的方法中发生:

  • [super layoutAttributesForElementsInRect:]只在里面 重载[layoutAttributesForElementsInRect:]

  • [super layoutAttributesForItemAtIndexPath:]仅在重载的[layoutAttributesForItemAtIndexPath:]内,

这些方法之间不应该发生交叉调用,至少indexPaths不是由相应的" super"提供的。方法,因为我们并不确切知道内发生了什么。

我在很长一段时间内与我的简历作斗争,并最终以唯一的工作顺序结束:

  1. 通过直接访问dataSource来准备添加布局数据(不需要调整CV的numberOfItemsInSection :),并将该数据存储在子类中。对象,例如在字典属性中,使用indexPath作为键。我在重载[prepareLayout]

  2. 中这样做
  3. 通过回调请求存储的布局数据到基类时:

  4. // layoutAttributesForElementsInRect

    - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    
    //calls super to get the array of layoutAttributes initialised by the base class
    **NSArray *array = [super layoutAttributesForElementsInRect:rect];**
    
    for(MyLayoutAttributes *la in array)
    
        if(la.representedElementCategory == UICollectionElementCategoryCell ){
           NSIndexPath indexPath =  la.indexPath //only this indexPath can be used during the call to layoutAttributesForItemAtIndexPath:!!! 
           //extracts custom layout data from a layouts dictionary
           MyLayoutAttributes *cellLayout = layouts[la.indexPath];  
           //sets additional properties
            la.this = cellLayout.this
            la.that = cellLayout.that
            ...
        }
       ....
    return array;
    }
    

    // layoutAttributesForItemAtIndexPath:

    - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
    
      MyLayoutAttributes *la = (MyLayoutAttributes *)[super layoutAttributesForItemAtIndexPath:indexPath ];
    
        if(la.representedElementCategory == UICollectionElementCategoryCell ){
            NSIndexPath indexPath =  la.indexPath //only this indexPath can be used during the call !!! 
           //extracts custom layout data from a layouts dictionary using indexPath as a key
           MyLayoutAttributes *cellLayout = layouts[la.indexPath];  
           //sets additional properties
            la.this = cellLayout.this
            la.that = cellLayout.that
        }
    return la;
    }
    

答案 1 :(得分:3)

我现在遇到类似的崩溃,即布局与数据源不同步。这个我可以复制,我相信我找到了原因。由于它是相似的,我想知道它是否与原始崩溃/问题的原因相同。错误是:

  

UICollectionView收到索引路径不存在的单元格的[sic]布局属性

我的UICollectionViewFlowLayout子类实现shouldInvalidateLayoutForBoundsChange:(这样我可以在用户滚动时将节标题放在可查看边界的顶部)。如果发生边界更改并且该方法返回YES,则系统会请求轻量级布局失效,即传递给布局的失效上下文中的所有属性都为NO:{{1} },invalidateEverythinginvalidateDataSourceCountsinvalidateFlowLayoutAttributes。如果之后调用invalidateFlowLayoutDelegateMetrics(大概是在布局重新处理之前),那么系统将不会自动调用reloadData,即它不会触发重量级的失效上下文,其中所有这些属性都是{{ 1}}。因此,当发生这种情况时,布局显然不会更新其部分/项目计数等的内部缓存,这会导致布局与数据源不同步并导致崩溃。

因此,对于我刚遇到的特殊情况,我可以通过在致电invalidateLayout后自行致电YES来解决这个问题。我不确定这是否与我原来的问题/崩溃情况相同,但确实听起来像是这样。

答案 2 :(得分:1)

我有同样的问题。我修好了。

__weak typeof(self) weakSelf = self;
UICollectionView *collectionView = self.collectionView;
[UIView performWithoutAnimation:^{
    [collectionView performBatchUpdates:^{
        collectionView.collectionViewLayout = layout;
    } completion:^(BOOL finished) {
        __strong typeof(self) strongSelf = weakSelf;
        if (strongSelf){
            [strongSelf reloadThumbData];
        }
    }];
}];

答案 3 :(得分:0)

虽然在我的情况下使用自定义布局,但我遇到了同样的问题。 我的解决方案是打电话:

  • insertItems
  • deleteItems
  • updateItems
适当地在collectionView上

以确保dataSource和layout知道相同数量的项。

我的布局基于从以下位置返回的数据:

super.layoutAttributesForElements(in: rect)

没有返回dataSource指示的预期数量的项目。

insertItems的文档似乎证实了这一点:

Call this method to insert one or more new items into the collection 
view. You might do this when your data source object receives data for 
new items or in response to user interactions with the collection view. 
The collection view gets the layout information for the new cells as 
part of calling this method.