11

I am trying to customise the positions of the headers in a UICollectionView using a subclassed UICollectionViewFlowLayout class (based loosely on the code for stacked headers which is shown enter link description here).

As a minimal test, let's say I just want to add a fixed offset to the position of all headers:

  • I add all headers to the array returned by layoutAttributesForElementsInRect so that all are always processed (this may be the cause of the problem, I'm not sure)
  • I then update each header by adding a fixed offset in layoutAttributesForSupplementaryViewOfKind

The full implementation is included at the end of this post.

(By the way, I know that adding all headers, including those outside the rect, is not strictly speaking necessary in the first step, but this is a simplified example of a more complex customisation in position I want to make which would cause all headers to be displayed in the draw rect.)

However, when I run the code I get the following 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'

It seems that this is caused by the update of the attributes, as if I comment out the following two lines it works fine:

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

What is causing this error, and what can I do to get my simple example up and running?

Here is the full class implementation for this simple example:

@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
Community
  • 1
  • 1
Ken Chatfield
  • 3,277
  • 3
  • 22
  • 27

6 Answers6

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

In my experience, the NSInternalInconsistencyException with the description above is thrown when the array returned from layoutAttributesForElementsInRect: contains two UICollectionViewLayoutAttributes objects with the same index path and (supplementary) element category.

titaniumdecoy
  • 18,900
  • 17
  • 96
  • 133
  • This is exactly what I was doing; it's easy if you're calling super and manipulating the resulting array. Thanks! – Jesse Rusak Jul 15 '14 at 18:21
  • 1
    Another possibility is that even though the UICollectionViewLayoutAttributes you set for the header is unique within the array of attributes you return in layoutAttributesForElementsInRect:, the CollectionView has kept another one among those it will display. The solution is then to keep a ref to this particular attribute and modify it, rather than recreating a new one every time. – Guillaume Laurent Jan 21 '15 at 09:13
  • Would give you a couple more uproots if I could! – Tokuriku Sep 11 '15 at 13:30
  • so what's the solution for this? – Moisés Olmedo Oct 03 '16 at 21:04
  • would be interested in this as well; how do I keep a ref to `this particular attribute`? – swalkner Oct 19 '16 at 13:26
  • This should be accepted answer because it really helped me solve the problem. – Josip B. Dec 01 '16 at 13:14
  • @GuillaumeLaurent The collection view does not "keep another [layout attribute with the same index path] among those it will display." You control all layout attributes that are returned by layoutAttributesForElementsInRect (and their corresponding index paths). If you choose to include elements returned by [super layoutAttributesForElementsInRect], you must ensure that the index paths of those layout attributes do not conflict with your own. Keeping a reference to and modifying the returned layout attribute(s) is unnecessary and will likely result in an internal inconsistency exception. – titaniumdecoy Dec 01 '16 at 16:57
  • @titaniumdecoy I don't remember exactly what error I had at the time that had led me to the conclusion detailed in my comment, but it's quite possible that the cause was indeed duplicates from calling super.layoutAttributesForElementInRect. Thank you for you correction. – Guillaume Laurent Dec 13 '16 at 13:43
3

You're receiving this error because you're adjusting the frame from (0 0; 320 50) to (0 50; 320 50) without re-validating the layout (likely you're doing this inadvertently).

Typically, it is because you're referencing the same IndexPath for two different layout elements but providing a different frame value for each.

Consider the following:

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];

Each is using the same IndexPath and thus it causes an NSInternalInconsistencyException

RndmTsk
  • 1,704
  • 2
  • 12
  • 12
1

OK, I'm not 100% sure why, but replacing the layoutAttributesForElementsInRect with the following seemed to do the trick:

- (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;
}

I can only imagine that before layoutAttributesForElementsInRect was being called early before the header I had added to the control for the first section was properly initialised, and so programatically determining what headers were present avoided this? Any thoughts would be welcome, but with the above the issue is resolved.

Ken Chatfield
  • 3,277
  • 3
  • 22
  • 27
0

To me this issue was occurring due to Sticky Header Layout, I solved it using PDKTStickySectionHeadersCollectionViewLayout

Aqib Mumtaz
  • 4,936
  • 1
  • 36
  • 33
0

This happened to me when sectionHeadersPinToVisibleBounds was set to true.

By overriding and passing true in func shouldInvalidateLayout(forBoundsChange: CGRect) rectified it. However, I am not sure of any other side effects that this solution would bring.

Sudara
  • 4,769
  • 3
  • 34
  • 39
0

The accepted answer (by titaniumdecoy) is correct. I just wanted to share my own experience with this issue as well as the solution I came up with.

I was using a custom decorator to create a divider (separators) between cells and after a while I decided to add headers to sections as well and this caused the internal inconsistency crash.

The solution was to check the indexPath of the current item in the layouts loop and skip the whole loop for that item if it's the first item in it's section.

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

}

And here is the separator view (it's not directly related to the question but maybe it's useful for future readers)

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");
    }
}
Vahid Amiri
  • 10,769
  • 13
  • 68
  • 113