UICollectionView flowLayout не корректно обертывает ячейки (iOS)

У меня есть UICollectionView с FLowLayout. Он будет работать, как я ожидаю большую часть времени, но время от времени одна из ячеек не обертывается должным образом. Например, ячейка, которая должна быть включена в первом "столбце" третьей строки, если на самом деле заканчивается во второй строке, и есть просто пустое место, где оно должно быть (см. Диаграмму ниже). Все, что вы можете видеть в этой ружевой ячейке, - это левая сторона (остальные отрезаны), и место, где оно должно быть, пусто.

Это не происходит последовательно; это не всегда одна и та же строка. Как только это произойдет, я могу прокручивать вверх, а затем обратно, и ячейка будет исправлена. Или, когда я нажимаю на ячейку (которая приводит меня к следующему виду через нажатие), а затем отбрасывается, я вижу ячейку в неправильном положении, а затем она переместится в правильное положение.

Скорость прокрутки, по-видимому, облегчает воспроизведение проблемы. Когда я медленно прокручиваю, я все равно вижу ячейку в неправильном положении время от времени, но затем она сразу же переместится в правильное положение.

Проблема началась, когда я добавил вставки разделов. Раньше у меня были ячейки, почти сливающиеся с границами коллекции (маленькие или без вставок), и я не заметил проблемы. Но это означало, что правый и левый вид коллекции пуст. Т.е., не удалось прокрутить. Кроме того, полоса прокрутки не была скрыта вправо.

Я могу решить проблему как на Simulator, так и на iPad 3.

Я предполагаю, что проблема происходит из-за левых и правых секций вставки... Но если значение неверно, я бы ожидал, что поведение будет последовательным. Интересно, может ли это быть ошибкой с Apple? Или, возможно, это связано с созданием вставки или чего-то подобного...

Любые решения/обходные пути оценены.

Illustration of problem and settings


Последующие действия: я использовал этот ответ ниже Nick за более чем 6 месяцев 12 месяцев 2 года без проблем (в случае, если люди задаются вопросом, есть ли какие-либо дыры в этом ответе - я еще не нашел их). Молодец Ник.

Ответ 1

В UICollectionViewFlowLayout есть ошибка в реализации layoutAttributesForElementsInRect, которая заставляет его возвращать объекты атрибутов TWO для одной ячейки в определенных случаях, включая секционные вставки. Один из возвращаемых объектов атрибута недействителен (вне границ представления коллекции), а другой действителен. Ниже приведен подкласс UICollectionViewFlowLayout, который устраняет проблему, исключая ячейки вне границ представления коллекции.

// NDCollectionViewFlowLayout.h
@interface NDCollectionViewFlowLayout : UICollectionViewFlowLayout
@end

// NDCollectionViewFlowLayout.m
#import "NDCollectionViewFlowLayout.h"
@implementation NDCollectionViewFlowLayout
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
  NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
  NSMutableArray *newAttributes = [NSMutableArray arrayWithCapacity:attributes.count];
  for (UICollectionViewLayoutAttributes *attribute in attributes) {
    if ((attribute.frame.origin.x + attribute.frame.size.width <= self.collectionViewContentSize.width) &&
        (attribute.frame.origin.y + attribute.frame.size.height <= self.collectionViewContentSize.height)) {
      [newAttributes addObject:attribute];
    }
  }
  return newAttributes;
}
@end

См. this.

Другие ответы предлагают вернуть YES из shouldInvalidateLayoutForBoundsChange, но это приводит к ненужным повторным вычислениям и даже не полностью решает проблему.

Мое решение полностью решает проблему и не должно вызывать никаких проблем, когда Apple исправляет основную причину.

Ответ 2

Я обнаружил подобные проблемы в своем iPhone-приложении. Поиск в форуме Apple dev принес мне это подходящее решение, которое работало в моем случае и, вероятно, также в вашем случае:

Подкласс UICollectionViewFlowLayout и переопределить shouldInvalidateLayoutForBoundsChange, чтобы вернуться YES.

//.h
@interface MainLayout : UICollectionViewFlowLayout
@end

и

//.m
#import "MainLayout.h"
@implementation MainLayout
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
    return YES;
}
@end

Ответ 3

Поместите это в viewController, которому принадлежит представление коллекции

- (void)viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];
    [self.collectionView.collectionViewLayout invalidateLayout];
}

Ответ 4

Быстрая версия ответа Ника Снайдера:

class NDCollectionViewFlowLayout : UICollectionViewFlowLayout {
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElements(in: rect)
        let contentSize = collectionViewContentSize
        return attributes?.filter { $0.frame.maxX <= contentSize.width && $0.frame.maxY < contentSize.height }
    }
}

Ответ 5

У меня была эта проблема, а также для базовой схемы gridview с вставками для полей. Ограниченная отладка, которую я выполнил на данный момент, реализует - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect в моем подклассе UICollectionViewFlowLayout и регистрирует, что возвращает реализация суперкласса, что явно показывает проблему.

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSArray *attrsList = [super layoutAttributesForElementsInRect:rect];

    for (UICollectionViewLayoutAttributes *attrs in attrsList) {
        NSLog(@"%f %f", attrs.frame.origin.x, attrs.frame.origin.y);
    }

    return attrsList;
}

Внедряя - (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath, я также вижу, что он возвращает неправильные значения для itemIndexPath.item == 30, который является фактором 10 моего числа gridview ячеек в строке, не уверен, что это релевантно.

- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath {
    UICollectionViewLayoutAttributes *attrs = [super initialLayoutAttributesForAppearingItemAtIndexPath:itemIndexPath];

    NSLog(@"initialAttrs: %f %f atIndexPath: %d", attrs.frame.origin.x, attrs.frame.origin.y, itemIndexPath.item);

    return attrs;
}

С отсутствием времени для большей отладки, обходной путь, который я сделал на данный момент, уменьшает мою ширину коллекции, равную левому и правому краю. У меня есть заголовок, который по-прежнему нуждается в полной ширине, поэтому я установил clipToBounds = NO в моем представлении коллекции, а затем удалил левую и правую вставки на нем, похоже, работает. Для просмотра заголовка, а затем оставайтесь на месте, вам нужно реализовать сдвиг и калибровку кадра в методах макета, которым поручено возвращать layoutAttributes для представления заголовка.

Ответ 6

Я добавил отчет об ошибке в Apple. Что для меня работает, это установить нижнюю секциюInset на значение, меньшее, чем верхняя вставка.

Ответ 7

Я столкнулся с той же проблемой смены ячеек на iPhone с помощью UICollectionViewFlowLayout, и поэтому я был рад найти ваш пост. Я знаю, что у вас проблема на iPad, но я публикую это, потому что думаю, что это общая проблема с UICollectionView. Итак, вот что я узнал.

Я могу подтвердить, что sectionInset относится к этой проблеме. Кроме того, headerReferenceSize также влияет на то, отключена ли ячейка или нет. (Это имеет смысл, так как это необходимо для расчета происхождения.)

К сожалению, необходимо учитывать даже разные размеры экрана. Когда вы играли со значениями для этих двух свойств, я испытал, что определенная конфигурация работала как на обоих (3,5 ", так и на 4" ), ни на одном, ни на одном из размеров экрана. Обычно ни один из них. (Это также имеет смысл, так как границы UICollectionView меняются, поэтому я не испытывал различий между сетчаткой и не сетчаткой.)

В итоге я установил sectionInset и headerReferenceSize в зависимости от размера экрана. Я пробовал около 50 комбинаций, пока не нашел значения, по которым проблема больше не возникала, и макет был визуально приемлемым. Очень сложно найти значения, которые работают на обоих размерах экрана.

Итак, резюмируя, я просто могу порекомендовать вам поиграть со значениями, проверить их на разных размерах экрана и надеяться, что Apple исправит эту проблему.

Ответ 8

Я просто испытал подобную проблему, но нашел совсем другое решение.

Я использую пользовательскую реализацию UICollectionViewFlowLayout с горизонтальной прокруткой. Я также создаю пользовательские расположения кадров для каждой ячейки.

Проблема, с которой я столкнулась, заключалась в том, что [super layoutAttributesForElementsInRect: rect] фактически не возвращал все UICollectionViewLayoutAttributes, которые должны отображаться на экране. При вызовах [self.collectionView reloadData] некоторые из ячеек внезапно будут установлены в скрытые.

В результате я создал NSMutableDictionary, который кэшировал все UICollectionViewLayoutAttributes, которые я видел до сих пор, а затем включать любые элементы, которые, как я знаю, должны отображаться.

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {

    NSArray * originAttrs = [super layoutAttributesForElementsInRect:rect];
    NSMutableArray * attrs = [NSMutableArray array];
    CGSize calculatedSize = [self calculatedItemSize];

    [originAttrs enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes * attr, NSUInteger idx, BOOL *stop) {
        NSIndexPath * idxPath = attr.indexPath;
        CGRect itemFrame = [self frameForItemAtIndexPath:idxPath];
        if (CGRectIntersectsRect(itemFrame, rect))
        {
            attr = [self layoutAttributesForItemAtIndexPath:idxPath];
            [self.savedAttributesDict addAttribute:attr];
        }
    }];

    // We have to do this because there is a bug in the collection view where it won't correctly return all of the on screen cells.
    [self.savedAttributesDict enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSArray * cachedAttributes, BOOL *stop) {

        CGFloat columnX = [key floatValue];
        CGFloat leftExtreme = columnX; // This is the left edge of the element (I'm using horizontal scrolling)
        CGFloat rightExtreme = columnX + calculatedSize.width; // This is the right edge of the element (I'm using horizontal scrolling)

        if (leftExtreme <= (rect.origin.x + rect.size.width) || rightExtreme >= rect.origin.x) {
            for (UICollectionViewLayoutAttributes * attr in cachedAttributes) {
                [attrs addObject:attr];
            }
        }
    }];

    return attrs;
}

Вот категория для NSMutableDictionary, что UICollectionViewLayoutAttributes сохраняются правильно.

#import "NSMutableDictionary+CDBCollectionViewAttributesCache.h"

@implementation NSMutableDictionary (CDBCollectionViewAttributesCache)

- (void)addAttribute:(UICollectionViewLayoutAttributes*)attribute {

    NSString *key = [self keyForAttribute:attribute];

    if (key) {

        if (![self objectForKey:key]) {
            NSMutableArray *array = [NSMutableArray new];
            [array addObject:attribute];
            [self setObject:array forKey:key];
        } else {
            __block BOOL alreadyExists = NO;
            NSMutableArray *array = [self objectForKey:key];

            [array enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *existingAttr, NSUInteger idx, BOOL *stop) {
                if ([existingAttr.indexPath compare:attribute.indexPath] == NSOrderedSame) {
                    alreadyExists = YES;
                    *stop = YES;
                }
            }];

            if (!alreadyExists) {
                [array addObject:attribute];
            }
        }
    } else {
        DDLogError(@"%@", [CDKError errorWithMessage:[NSString stringWithFormat:@"Invalid UICollectionVeiwLayoutAttributes passed to category extension"] code:CDKErrorInvalidParams]);
    }
}

- (NSArray*)attributesForColumn:(NSUInteger)column {
    return [self objectForKey:[NSString stringWithFormat:@"%ld", column]];
}

- (void)removeAttributesForColumn:(NSUInteger)column {
    [self removeObjectForKey:[NSString stringWithFormat:@"%ld", column]];
}

- (NSString*)keyForAttribute:(UICollectionViewLayoutAttributes*)attribute {
    if (attribute) {
        NSInteger column = (NSInteger)attribute.frame.origin.x;
        return [NSString stringWithFormat:@"%ld", column];
    }

    return nil;
}

@end

Ответ 9

Вышеуказанные ответы не работают для меня, но после загрузки изображений я заменил

[self.yourCollectionView reloadData]

с

[self.yourCollectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];

для обновления, и он может отображать все ячейки правильно, вы можете попробовать.

Ответ 10

Я только что столкнулся с аналогичной проблемой с исчезновением ячеек после прокрутки UICollectionView на iOS 10 (не было проблем с iOS 6-9).

Подклассификация UICollectionViewFlowLayout и переопределение метода layoutAttributesForElementsInRect: не работает в моем случае.

Решение было достаточно простым. В настоящее время я использую экземпляр UICollectionViewFlowLayout и устанавливаю как itemSize, так и valuItemSize (раньше я не использовал valuItemSize) и устанавливал его на некоторый ненулевой размер. Фактический размер вычисляется в коллекцииView: layout: sizeForItemAtIndexPath: метод.

Кроме того, я удалил вызов метода invalidateLayout из layoutSubviews, чтобы избежать ненужных перезагрузок.