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, чтобы избежать ненужных перезагрузок.