在 UICollectionView 布局中自定义标题的位置会导致 NSInternalInconsistencyException 错误

Posted

技术标签:

【中文标题】在 UICollectionView 布局中自定义标题的位置会导致 NSInternalInconsistencyException 错误【英文标题】:Customising position of header in UICollectionView layout causes NSInternalInconsistencyException error 【发布时间】:2014-01-15 00:45:53 【问题描述】:

我正在尝试使用子类 UICollectionViewFlowLayout 类自定义 UICollectionView 中标题的位置(松散地基于显示为 enter link description here 的堆叠标题的代码)。

作为一个最小的测试,假设我只想在所有标题的位置添加一个固定的偏移量:

我将所有标头添加到layoutAttributesForElementsInRect 返回的数组中,以便始终处理所有标头(这可能是问题的原因,我不确定) 然后我通过在layoutAttributesForSupplementaryViewOfKind 中添加一个固定偏移量来更新每个标题

完整的实现包含在本文末尾。

(顺便说一句,我知道在第一步中添加所有标题,包括矩形之外的标题,严格来说并不是必需的,但这是我想要进行的更复杂的位置自定义的简化示例,它将导致所有标题都显示在绘图矩形中。)

但是,当我运行代码时,我得到以下NSInternalInconsistencyException

2014-01-15 00:41:50.130 CollectionStackedHeaders[60777:70b] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException',
reason: 'layout attributes for supplementary item at index path (<NSIndexPath: 0x8a7db90> length = 2, path = 0 - 0)
changed from <UICollectionViewLayoutAttributes: 0x8a7f8b0> index path: (<NSIndexPath: 0x8a7d9c0> length = 2, path = 0 - 0); element kind: (UICollectionElementKindSectionHeader); frame = (0 0; 320 50);
        to   <UICollectionViewLayoutAttributes: 0x8a7fb80> index path: (<NSIndexPath: 0x8a7db90> length = 2, path = 0 - 0); element kind: (UICollectionElementKindSectionHeader); frame = (0 50; 320 50); zIndex = 1024;
without invalidating the layout'

看来这是属性更新造成的,好像我注释掉下面两行就可以正常工作了:

attributes.zIndex = 1024;
attributes.frame = frame;

是什么导致了这个错误,我该怎么做才能让我的简单示例启动并运行?

这是这个简单示例的完整类实现:

@implementation myStackedHeaderFlowLayout

- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect 
    // Call super to get elements
    NSMutableArray* answer = [[super layoutAttributesForElementsInRect:rect] mutableCopy];

    // As a test, always add first header to the answer array
    NSArray* indexes = [NSArray arrayWithObjects: [NSNumber numberWithInt:0], nil];
    for (NSNumber* sectionNumber in indexes) 
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:[sectionNumber integerValue]];
        UICollectionViewLayoutAttributes* layoutAttributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath];
        if (layoutAttributes) 
            [answer removeObject:layoutAttributes]; // remove if already present
            [answer addObject:layoutAttributes];
        
    

    return answer;


- (UICollectionViewLayoutAttributes*)layoutAttributesForSupplementaryViewOfKind:(NSString*)kind atIndexPath:(NSIndexPath*)indexPath 
    // Call super to get base attributes
    UICollectionViewLayoutAttributes* attributes = [super layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:indexPath];

    if ([kind isEqualToString:UICollectionElementKindSectionHeader]) 
        CGRect frame = attributes.frame;
        frame.origin.y += 50;
        // Update attributes position here - causes the problem
        attributes.zIndex = 1024;
        attributes.frame = frame;
    

    return attributes;


- (UICollectionViewLayoutAttributes*)initialLayoutAttributesForAppearingSupplementaryElementOfKind:(NSString*)kind atIndexPath:(NSIndexPath*)indexPath 
    UICollectionViewLayoutAttributes* attributes = [self layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:indexPath];
    return attributes;


- (UICollectionViewLayoutAttributes*)finalLayoutAttributesForDisappearingSupplementaryElementOfKind:(NSString*)kind atIndexPath:(NSIndexPath*)indexPath 
    UICollectionViewLayoutAttributes* attributes = [self layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:indexPath];
    return attributes;


- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBound 
    return YES;


@end

【问题讨论】:

【参考方案1】:
layout attributes for supplementary item at index path (<NSIndexPath>) 
changed from <UICollectionViewLayoutAttributes> 
to <UICollectionViewLayoutAttributes>
without invalidating the layout

根据我的经验,当layoutAttributesForElementsInRect: 返回的数组包含两个具有相同索引路径 和(补充)元素类别的UICollectionViewLayoutAttributes 对象时,会抛出具有上述描述的NSInternalInconsistencyException

【讨论】:

这正是我正在做的;如果您调用 super 并操作结果数组,这很容易。谢谢! 另一种可能性是,即使您为标题设置的 UICollectionViewLayoutAttributes 在您在 layoutAttributesForElementsInRect: 中返回的属性数组中是唯一的,CollectionView 仍会在将显示的属性中保留另一个。解决方案是保留此特定属性的 ref 并对其进行修改,而不是每次都重新创建一个新的。 如果可以的话,我会给你更多的连根拔起! 那有什么解决办法呢? 也会对此感兴趣;如何保留对this particular attribute 的引用?【参考方案2】:

您收到此错误是因为您在未重新验证布局的情况下将 frame(0 0; 320 50) 调整为 (0 50; 320 50)(可能是您无意中这样做了)。

通常,这是因为您为两个不同的布局元素引用了相同的 IndexPath,但为每个元素提供了不同的 frame 值。

考虑以下几点:

NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];

UICollectionViewLayoutAttributes *newAttribute1 = [UICollectionViewLayoutAttributes layoutAttributesForDecorationViewOfKind:UICollectionElementKindSectionHeader withIndexPath:indexPath];
newAttribute1.frame = CGRectMake(0, 50, 320, 50);
[attributes addObject:newAttribute1];

UICollectionViewLayoutAttributes *newAttribute2 = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionFooter withIndexPath:indexPath];
newAttribute2.frame = CGRectMake(0, 0, 320, 50);
[attributes addObject:newAttribute2];

每个都使用相同的IndexPath,因此会导致NSInternalInconsistencyException

【讨论】:

【参考方案3】:

好的,我不是 100% 确定原因,但是用以下内容替换 layoutAttributesForElementsInRect 似乎可以解决问题:

- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect 
    // Call super to get elements
    NSMutableArray* answer = [[super layoutAttributesForElementsInRect:rect] mutableCopy];

    NSUInteger maxSectionIndex = 0;
    for (NSUInteger idx=0; idx < [answer count]; ++idx) 
        UICollectionViewLayoutAttributes *layoutAttributes = answer[idx];

        if (layoutAttributes.representedElementCategory == UICollectionElementCategoryCell || layoutAttributes.representedElementCategory == UICollectionElementCategorySupplementaryView) 
            // Keep track of the largest section index found in the rect (maxSectionIndex)
            NSUInteger sectionIndex = (NSUInteger)layoutAttributes.indexPath.section;
            if (sectionIndex > maxSectionIndex) 
                maxSectionIndex = sectionIndex;
            
        
        if ([layoutAttributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) 
            // Remove layout of header done by our super, as we will do it right later
            [answer removeObjectAtIndex:idx];
            idx--;
        
    

    // Re-add all section headers for sections >= maxSectionIndex
    for (NSUInteger idx=0; idx <= maxSectionIndex; ++idx) 
        NSIndexPath* indexPath = [NSIndexPath indexPathForItem:0 inSection:idx];
        UICollectionViewLayoutAttributes *layoutAttributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath];
        if (layoutAttributes) 
            [answer addObject:layoutAttributes];
        
    

    return answer;

我只能想象在 layoutAttributesForElementsInRect 被调用之前,在我添加到第一部分控件的标题被正确初始化之前,因此以编程方式确定存在哪些标题避免了这种情况?欢迎提出任何想法,但上述问题已解决。

【讨论】:

【参考方案4】:

对我来说,这个问题是由于标头布局引起的,我使用 PDKTStickySectionHeadersCollectionViewLayout 解决了它

【讨论】:

【参考方案5】:

sectionHeadersPinToVisibleBounds 设置为 true 时,这发生在我身上。

通过在func shouldInvalidateLayout(forBoundsChange: CGRect) 中覆盖和传递true 来纠正它。但是,我不确定此解决方案会带来任何其他副作用。

【讨论】:

【参考方案6】:

接受的答案(titaniumdecoy)是正确的。我只是想分享我自己在这个问题上的经验以及我想出的解决方案。

我使用自定义装饰器在单元格之间创建分隔符(分隔符),过了一段时间我决定也向部分添加标题,这导致内部不一致崩溃。

解决方案是检查布局循环中当前项目的indexPath,并跳过该项目的整个循环如果它是其部分中的第一项

final class SingleItemWithSeparatorFlowLayout: UICollectionViewFlowLayout 

    var skipFirstItem: Bool = false;

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? 
        let layoutAttributes = super.layoutAttributesForElements(in: rect) ?? [];
        let lineWidth = self.minimumLineSpacing;

        var decorationAttributes: [UICollectionViewLayoutAttributes] = [];

        for layoutAttribute in layoutAttributes where skipFirstItem ? (layoutAttribute.indexPath.item > 0) : true 
            // skip the first item in each section
            if(layoutAttribute.indexPath.item == 0) 
                continue;
            

            let separatorAttribute = UICollectionViewLayoutAttributes(forDecorationViewOfKind: SeparatorView.ID, with: layoutAttribute.indexPath);
            let cellFrame = layoutAttribute.frame;
            separatorAttribute.frame = CGRect(x: cellFrame.origin.x, y: cellFrame.origin.y, width: cellFrame.size.width, height: lineWidth);
            separatorAttribute.zIndex = Int.max;
            decorationAttributes.append(separatorAttribute);
        

        return layoutAttributes + decorationAttributes;
    


这里是分隔符视图(它与问题没有直接关系,但可能对未来的读者有用)

final class SeparatorView: UICollectionReusableView 

    static let ID = "SeparatorView";

    override init(frame: CGRect) 
        super.init(frame: frame);
        self.backgroundColor = UIColor.lightGray.withAlphaComponent(0.5);
    

    override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) 
        self.frame = layoutAttributes.frame;
    

    required init?(coder aDecoder: NSCoder) 
        fatalError("init(coder:) has not been implemented");
    

【讨论】:

以上是关于在 UICollectionView 布局中自定义标题的位置会导致 NSInternalInconsistencyException 错误的主要内容,如果未能解决你的问题,请参考以下文章

如何在Android中自定义相机布局?

Android 中自定义ViewGroup实现流式布局的效果

UICollectionView 中的自定义布局

Swift 中的 UICollectionView 自定义布局

android xml,布局中自定义视图的崩溃

使用 Swift 3 在 UICollectionView 上自定义布局