没有正确包装单元格

我有一个带有 FlowLayout 的 UICollectionView。它将工作,因为我期望的大多数时间,但不时有一个细胞不包装正确。例如,在第三行的第一个“列”中应该打开的单元格,如果它实际上在第二行的尾部,并且在它应该在的地方只有一个空白(见下图)。所有你可以看到的这个胭脂细胞是左手边(其余的被切断)和地方,它应该是空的。

这种情况并不总是发生; 它并不总是相同的行。一旦它发生了,我可以向上滚动,然后回来,细胞将自我修复。或者,当我按下单元格(通过推动将我带到下一个视图)然后弹回,我将看到单元格在不正确的位置,然后它将跳到正确的位置。

滚动速度似乎更容易重现这个问题。当我慢慢滚动时,我仍然可以看到细胞在错误的位置时不时地,但然后它会跳到正确的位置立即。

这个问题是从我添加节插入时开始的。以前,我让单元格几乎对集合边界(很少或没有插入)进行刷新,但是没有注意到这个问题。但这意味着 Collection 视图的左右两边是空的。我,不能滚动。此外,滚动条没有向右刷新。

我可以让这个问题同时发生在 Simulator 和 iPad3上。

我猜测问题的发生是因为左侧和右侧部分的插入... 但是如果值是错误的,那么我希望行为是一致的。我想知道这是不是苹果的一个 bug?或者,这可能是由于嵌入或类似的东西的建立。

Illustration of problem and settings


跟进 : 我已经使用 Nick 下面的 这个答案超过2年了,没有出现任何问题(以防人们想知道这个答案是否有任何漏洞——我还没有发现任何漏洞)。干得好,尼克。

45826 次浏览

对于一个基本的网格视图布局,我也遇到过类似的问题。我现在所做的有限的调试是在我的 UICollectionViewFlowLayout 子类中实现 - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect,并记录超类实现返回的内容,这清楚地显示了问题。

- (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,不确定这是否相关。

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

由于缺乏进行更多调试的时间,我现在所做的工作方法是减少我的 Collectionviews 宽度,使其等于左边距和右边距。我有一个头,仍然需要全宽,所以我已经设置 clipsToBounds = NO 在我的 Collectionview,然后也删除了它的左右插入,似乎工作。为了使头视图保持不变,您需要在布局方法中实现帧移位和大小调整,这些布局方法的任务是为头视图返回 layoutAttritribute。

我在使用 UICollectionViewFlowLayout的 iPhone 上遇到了同样的细胞替换问题,所以我很高兴找到了你的文章。我知道你在 iPad 上遇到了这个问题,但是我发布这篇文章是因为我认为这是 UICollectionView的一个普遍问题。以下是我的发现。

我可以确认 sectionInset和这个问题有关。除此之外,headerReferenceSize还对细胞是否移位有影响。(这是有道理的,因为它是计算原点所必需的。)

不幸的是,即使是不同的屏幕尺寸也必须考虑到。在处理这两个属性的值时,我体验到某种配置既可以在(3.5“和4”)上工作,也可以不工作,或者只在一个屏幕大小上工作。通常都不会。(这也是有道理的,因为 UICollectionView的界限发生了变化,所以我没有体验到视网膜和非视网膜之间的任何差异。)

我最终设置的 sectionInsetheaderReferenceSize取决于屏幕大小。我尝试了大约50个组合,直到我发现值下的问题不再发生,布局在视觉上是可以接受的。很难找到适用于两种屏幕大小的值。

所以总结一下,我只能建议你去玩玩这些值,在不同的屏幕尺寸上检查一下,希望苹果能够解决这个问题。

我在我的 iPhone 应用程序中发现了类似的问题。搜索苹果开发论坛给我带来了这个合适的解决方案,它在我的案例中起作用,可能也会在你的案例中起作用:

子类 UICollectionViewFlowLayout并重写 shouldInvalidateLayoutForBoundsChange以返回 YES

//.h
@interface MainLayout : UICollectionViewFlowLayout
@end

还有

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

我已经向苹果公司添加了一个错误报告。对我有效的方法是将 bottom sectionInset 设置为小于 top inset 的值。

在 UICollectionViewFlowLayout 的 layoutAttributesForElementsInRect 实现中存在一个 bug,导致它在某些情况下为一个单元格返回两个属性对象,这些情况涉及节插入。返回的属性对象之一是无效的(在集合视图的边界之外) ,另一个是有效的。下面是 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

参见 这个

其他答案建议从 should dInvalidateLayoutForBoundsChange 返回 YES,但这会导致不必要的重新计算,甚至不能完全解决问题。

我的解决方案完全解决了这个问题,并且在苹果解决根本原因时不会引起任何问题。

将其放入拥有集合视图的 viewController 中

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

我刚刚经历了一个类似的问题,但找到了一个非常不同的解决方案。

我使用的是 UICollectionViewFlowLayout 的自定义实现和水平滚动。我还为每个单元格创建自定义帧位置。

我的问题是[ super layoutAttributesForElementsInRect: rect ]并没有返回所有应该显示在屏幕上的 UICollectionViewLayoutAttribute。在调用[ self. CollectionView reloadData ]时,一些单元格会突然被设置为隐藏。

最后,我创建了一个 NSMutableDictionary,它缓存了我目前看到的所有 UICollectionViewLayoutAttritribute,然后包括我知道应该显示的任何项目。

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

下面是正确保存 UICollectionViewLayoutAttritribute 的 NSMutableDictionary 类别。

#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

上面的答案对我来说不起作用,但是在下载了这些图片之后,我替换了它们

[self.yourCollectionView reloadData]

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

要刷新,它可以正确显示所有单元格,您可以尝试。

尼克•斯奈德(Nick Snyder)回答的一个迅速版本:

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

我也遇到过类似的问题,在 IOS10上的 UICollectionView 滚动之后,单元格消失了(在 iOS6-9上没有问题)。

UICollectionViewFlowLayout 的子类化和重写方法 layoutAttributesForElementsInRect: 在我的例子中不起作用。

解决办法很简单。目前,我使用 UICollectionViewFlowLayout 的一个实例并设置 大小和估计的大小(我以前没有使用 EstiatedItemSize) ,并将其设置为一些非零大小。 实际大小是在 CollectionView: layout: sizeForItemAtIndexPath: method 中计算的。

另外,为了避免不必要的重新加载,我已经从 layoutSubviews 中删除了对否定布局方法的调用。

这可能有点晚,但是要确保在 prepare()中设置属性(如果可能的话)。

我的问题是细胞正在排列,然后在 layoutAttributesForElements中得到更新。这导致了新单元格进入视野时的闪烁效应。

通过将所有属性逻辑移动到 prepare,然后在 UICollectionViewCell.apply()中设置它们,它消除了闪烁,创建了黄油平滑的单元格显示

快速5版 尼克 · 斯奈德答案:

class NDCollectionViewFlowLayout: UICollectionViewFlowLayout {
open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
var newAttributes = [AnyHashable](repeating: 0, count: attributes?.count ?? 0)
for attribute in attributes ?? [] {
if (attribute.frame.origin.x + attribute.frame.size.width <= collectionViewContentSize.width) && (attribute.frame.origin.y + attribute.frame.size.height <= collectionViewContentSize.height) {
newAttributes.append(attribute)
}
}
return newAttributes as? [UICollectionViewLayoutAttributes]
}
}

或者你可以使用 UICollectionViewFlowLayout的扩展:

extension UICollectionViewFlowLayout {
open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
var newAttributes = [AnyHashable](repeating: 0, count: attributes?.count ?? 0)
for attribute in attributes ?? [] {
if (attribute.frame.origin.x + attribute.frame.size.width <= collectionViewContentSize.width) && (attribute.frame.origin.y + attribute.frame.size.height <= collectionViewContentSize.height) {
newAttributes.append(attribute)
}
}
return newAttributes as? [UICollectionViewLayoutAttributes]
}
}