UICollectionView flowLayout неправильно обертывает ячейки

79

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

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

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

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

Я могу сделать проблему как на Симуляторе, так и на iPad 3.

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

Иллюстрация проблемы и настроек


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

лисица
источник

Ответы:

94

В реализации layoutAttributesForElementsInRect UICollectionViewFlowLayout есть ошибка, которая заставляет возвращать ДВА объекта атрибутов для одной ячейки в некоторых случаях, связанных с вставками разделов. Один из возвращенных объектов атрибута недопустим (за пределами представления коллекции), а другой действителен. Ниже приведен подкласс 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

Смотрите это .

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

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

Ник Снайдер
источник
5
К вашему сведению, эта ошибка также возникает при горизонтальной прокрутке. Замена x на y и ширины на высоту заставляет этот патч работать.
Патрик Тешер
Благодаря! Я только начинал играть с collectionView (если вы хотите спамить Apple об этом, вот ссылка на rdar openradar.appspot.com/12433891 )
Vinzzz
1
@richarddas Нет, вы не хотите проверять, пересекаются ли прямые. Фактически, все ячейки (действительные или недопустимые) будут пересекать границы прямоугольника представления коллекции. Вы хотите проверить, выходит ли какая-либо часть прямоугольника за границы, что и делает мой код.
Ник Снайдер
2
@Rpranata В iOS 7.1 эта ошибка не исправлена. Вздох.
nonamelive
2
Возможно, я использую это неправильно, но в iOS 8.3 в Swift это приводит к тому, что подпредставления справа, которые раньше были обрезаны, не отображаются вообще. Кто-нибудь еще?
sudo
8

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

- (void)viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];
    [self.collectionView.collectionViewLayout invalidateLayout];
}
Питер Лапису
источник
Куда вы это положили?
fatuhoku
В viewController, который владеет представлением коллекции
Питер Лапису,
3
Моя проблема заключалась в том, что клетки полностью исчезли. Это решение помогло - однако это вызывает ненужные перезагрузки. Тем не менее, сейчас он работает .. спасибо!
pawi
1
это вызывает бесконечный цикл при вызове из viewController для меня
Hofi
Как отметил DHennessy13 , это текущее решение хорошее, но может быть несовершенным, так как оно сделает недействительнымLayout при повороте экрана (а в большинстве случаев этого не должно быть). Улучшение может заключаться в установке флага invalidateLayoutтолько один раз.
Cœur
7

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

Подкласс UICollectionViewFlowLayoutи переопределение shouldInvalidateLayoutForBoundsChangeдля возврата YES.

//.h
@interface MainLayout : UICollectionViewFlowLayout
@end

и

//.m
#import "MainLayout.h"
@implementation MainLayout
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
    return YES;
}
@end
xxtesaxx
источник
Это лишь частично решает проблему, с которой я столкнулся. Ячейка действительно переходит в нужное место, когда появляется строка. Однако прямо перед тем, как он появится, я все еще вижу, что сбоку появляется ячейка.
Дэниел Вуд
Осторожно - это приведет к запуску макета при каждой прокрутке. Это может серьезно повлиять на производительность.
fatuhoku
7

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

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 }
    }
}
Патрик Пийнаппель
источник
1
это полностью привело к исчезновению CollectionViewCell ... Любое другое возможное решение?
Гиггз
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;
}

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

моноэнергетик
источник
Спасибо за дополнительную информацию @monowerker. Я думаю, что моя проблема началась, когда я добавил вставки (я добавил это к вопросу). Я попробую ваши методы отладки и посмотрю, что они мне скажут. Я тоже могу попробовать твою работу.
lindon fox
Скорее всего, это ошибка в UICFL / UICL, я собираюсь попытаться зарегистрировать радар, когда у меня будет время, вот обсуждение с некоторыми номерами rdar, на которые вы можете ссылаться. twitter.com/steipete/status/258323913279410177
monowerker
4

Я добавил отчет об ошибке в Apple. Что мне подходит, так это установить для bottom sectionInset значение меньше верхнего inset.

DrMickeyLauer
источник
3

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

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

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

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

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

Маса
источник
3

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

Создание подклассов UICollectionViewFlowLayout и переопределение метода layoutAttributesForElementsInRect: в моем случае не работает.

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

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

Андрей Середкин
источник
где в uicollectionviewflowlayout заданы размеры элементов и примерный размер?
Garrett Cox
UICollectionViewFlowLayout * flowLayout = [[выделение UICollectionViewFlowLayout] инициализация]; [flowLayout setItemSize: CGSizeMake (200, 200)]; [flowLayout setEstimatedItemSize: CGSizeMake (200, 200)]; self.collectionView = [[UICollectionView alloc] initWithFrame: CGRectZero collectionViewLayout: flowLayout];
Андрей Середкин
Установка
EstimatedItemSize
2

У меня возникла аналогичная проблема, но я нашел совсем другое решение.

Я использую пользовательскую реализацию 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
Endama
источник
Я тоже использую горизонтальную прокрутку, мне удалось исправить проблему с помощью вашего решения, но после перехода к другому виду и возврата размер содержимого казался неправильным, когда есть дополнительные элементы, которые не делятся на столбцы поровну.
morph85
Я нашел решение, чтобы исправить проблему, когда ячейка скрывалась после выполнения перехода и возврата в представление коллекции. Старайтесь не устанавливать EstimatedItemSize в collectionViewFlowLayout; установить itemSize напрямую.
morph85
2

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

[self.yourCollectionView reloadData]

с участием

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

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

Яо Ли
источник
0

Это может быть немного поздно, но по возможности убедитесь, что вы устанавливаете свои атрибуты prepare().

Моя проблема заключалась в том, что ячейки выкладывались, а затем обновлялись layoutAttributesForElements. Это приводило к эффекту мерцания при появлении новых ячеек.

Переместив всю логику атрибутов prepare, а затем установив их в UICollectionViewCell.apply()ней, мы устранили мерцание и создали масляно-гладкую ячейку отображения 😊

Майкл
источник