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

时间:2014-01-15 00:45:54

标签: ios ios7 uicollectionview uicollectionviewlayout

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

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

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

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

(顺便说一句,我知道在第一步中添加所有标题,包括那些在rect之外的标题,并不是严格意义上的,但这是一个简单的例子,说明我希望制作的位置更复杂的定制导致所有标题显示在绘制矩形中。)

但是,当我运行代码时,我得到以下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

6 个答案:

答案 0 :(得分:16)

layout attributes for supplementary item at index path (<NSIndexPath>) 
changed from <UICollectionViewLayoutAttributes> 
to <UICollectionViewLayoutAttributes>
without invalidating the layout

根据我的经验,当从NSInternalInconsistencyException返回的数组包含两个{strong>相同索引路径的layoutAttributesForElementsInRect:个对象时,会抛出上面描述的UICollectionViewLayoutAttributes (补充)要素类别。

答案 1 :(得分:3)

您收到此错误是因为您正在将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

答案 2 :(得分:1)

好的,我不是百分之百确定原因,但用以下内容替换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之前已正确初始化,所以以编程方式确定哪些标题存在避免了这个?任何想法都会受到欢迎,但随着上述问题得到解决。

答案 3 :(得分:0)

对我来说,这个问题是由于Sticky Header Layout而发生的,我使用PDKTStickySectionHeadersCollectionViewLayout解决了它

答案 4 :(得分:0)

sectionHeadersPinToVisibleBounds设置为true时,我发生了这种情况。

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

答案 5 :(得分:0)

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");
    }
}