Настройка положения заголовка в макете UICollectionView вызывает ошибку NSInternalInconsistencyException

Я пытаюсь настроить позиции заголовков в UICollectionView, используя подкласс UICollectionViewFlowLayout (на основе кода для сложенных заголовков, который показан введите здесь описание ссылки).

В качестве минимального теста предположим, что я просто хочу добавить фиксированное смещение к положению всех заголовков:

  • Я добавляю все заголовки в массив, возвращаемый 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

person Ken Chatfield    schedule 15.01.2014    source источник


Ответы (6)


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

По моему опыту, NSInternalInconsistencyException с приведенным выше описанием возникает, когда массив, возвращенный из layoutAttributesForElementsInRect:, содержит два объекта UICollectionViewLayoutAttributes с одним и тем же путем индекса и (дополнительной) категорией элемента.

person titaniumdecoy    schedule 28.04.2014
comment
Это именно то, что я делал; это легко, если вы вызываете super и манипулируете результирующим массивом. Спасибо! - person Jesse Rusak; 15.07.2014
comment
Другая возможность состоит в том, что даже если UICollectionViewLayoutAttributes, который вы установили для заголовка, уникален в массиве атрибутов, которые вы возвращаете в layoutAttributesForElementsInRect:, CollectionView сохранил еще один среди тех, которые он будет отображать. Тогда решение состоит в том, чтобы сохранить ссылку на этот конкретный атрибут и изменить его, а не создавать каждый раз новый. - person Guillaume Laurent; 21.01.2015
comment
Если бы я мог, дал бы вам еще пару выкорчеванных корней! - person Tokuriku; 11.09.2015
comment
так какое решение для этого? - person Moisés Olmedo; 04.10.2016
comment
это тоже будет интересно; как сохранить ссылку на this particular attribute? - person swalkner; 19.10.2016
comment
Это должен быть принятый ответ, потому что он действительно помог мне решить проблему. - person Josip B.; 01.12.2016
comment
@GuillaumeLaurent Представление коллекции не сохраняет другой [атрибут макета с тем же путем индекса] среди отображаемых. Вы управляете всеми атрибутами макета, возвращаемыми layoutAttributesForElementsInRect (и их соответствующими путями индекса). Если вы решите включить элементы, возвращаемые [super layoutAttributesForElementsInRect], вы должны убедиться, что пути индекса этих атрибутов макета не конфликтуют с вашими собственными. Сохранение ссылки и изменение возвращаемых атрибутов макета не требуется и, скорее всего, приведет к внутреннему исключению несогласованности. - person titaniumdecoy; 01.12.2016
comment
@titaniumdecoy Я точно не помню, какая ошибка у меня была в то время, которая привела меня к выводу, подробно описанному в моем комментарии, но вполне возможно, что причиной действительно были дубликаты при вызове super.layoutAttributesForElementInRect. Спасибо за исправление. - person Guillaume Laurent; 13.12.2016

Вы получаете эту ошибку, потому что настраиваете 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

person RndmTsk    schedule 23.07.2015

Хорошо, я не уверен на 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 вызывался раньше, чем заголовок, который я добавил к элементу управления для первого раздела, был должным образом инициализирован, и поэтому программное определение того, какие заголовки присутствовали, избежало этого? Приветствуются любые мысли, но с вышеизложенным проблема решена.

person Ken Chatfield    schedule 15.01.2014

Для меня эта проблема возникла из-за липкого макета заголовка, я решил ее, используя PDKTStickySectionHeadersCollectionViewLayout

person Aqib Mumtaz    schedule 05.10.2016

Это случилось со мной, когда для sectionHeadersPinToVisibleBounds было установлено значение true.

Переопределив и передав true в func shouldInvalidateLayout(forBoundsChange: CGRect), исправил это. Однако я не уверен в каких-либо других побочных эффектах, которые принесет это решение.

person Sudara    schedule 23.02.2018

Принятый ответ (от 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");
    }
}
person Vahid Amiri    schedule 08.07.2018