在 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 中自定义ViewGroup实现流式布局的效果