79

我有UICollectionView一个 FLoLayout。它在大多数情况下都会像我预期的那样工作,但时不时地有一个单元格不能正确包裹。例如,应该在第三行的第一个“列”中的单元格如果实际上在第二行的尾随,并且它应该是一个空白区域(见下图)。你可以看到这个胭脂牢房的左侧(其余部分被切断),它应该是空的。

这不会始终如一地发生;它并不总是同一行。一旦它发生,我可以向上滚动然后返回,单元格将自行修复。或者,当我按下单元格(通过推送将我带到下一个视图)然后弹回时,我会看到单元格处于不正确的位置,然后它会跳转到正确的位置。

滚动速度似乎更容易重现问题。当我慢慢滚动时,我仍然可以看到单元格在错误的位置,但它会立即跳转到正确的位置。

当我添加部分插图时,问题就开始了。以前,我的单元格几乎与集合边界齐平(很少或没有插图),但我没有注意到问题。但这意味着集合视图的右侧和左侧是空的。即,无法滚动。此外,滚动条没有向右齐平。

我可以在模拟器和 iPad 3 上解决这个问题。

我猜这个问题是由于左右部分的插图而发生的......但是如果值是错误的,那么我希望行为是一致的。我想知道这是否可能是Apple的错误?或者这可能是由于插入或类似的东西的积累。

问题和设置说明


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

4

12 回答 12

95

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

看到这个

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

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

于 2012-11-14T23:55:31.473 回答
8

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

- (void)viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];
    [self.collectionView.collectionViewLayout invalidateLayout];
}
于 2014-06-03T08:48:32.770 回答
7

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

子类化UICollectionViewFlowLayout并覆盖shouldInvalidateLayoutForBoundsChange返回YES

//.h
@interface MainLayout : UICollectionViewFlowLayout
@end

//.m
#import "MainLayout.h"
@implementation MainLayout
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
    return YES;
}
@end
于 2012-10-31T20:06:06.977 回答
7

尼克斯奈德回答的 Swift 版本:

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 }
    }
}
于 2015-07-03T12:06:41.903 回答
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 的错误值,这是我的 gridview 每行单元格数的 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 宽度,数量等于左边距和右边距。我有一个仍然需要全宽的标题,所以我在我的 collectionview 上设置了 clipsToBounds = NO ,然后还删除了它的左右插图,似乎有效。为了使标题视图保持原位,您需要在布局方法中实现框架移动和调整大小,这些方法的任务是为标题视图返回 layoutAttributes。

于 2012-10-17T09:28:29.697 回答
4

我已向 Apple 添加了错误报告。对我有用的是将底部 sectionInset 设置为小于顶部插入的值。

于 2012-11-08T13:26:03.930 回答
3

我在 iPhone 上使用 a 时遇到了同样的单元格替换问题UICollectionViewFlowLayout,所以我很高兴找到你的帖子。我知道您在 iPad 上遇到问题,但我发布此内容是因为我认为这是UICollectionView. 所以这就是我发现的。

我可以确认这与sectionInset该问题有关。除此之外,headerReferenceSize它还影响细胞是否被取代。(这是有道理的,因为计算原点需要它。)

不幸的是,甚至必须考虑不同的屏幕尺寸。在玩弄这两个属性的值时,我体验到某种配置要么适用于(3.5 英寸和 4 英寸),要么不适用,要么只适用于其中一种屏幕尺寸。通常都没有。(这也是有道理的,因为UICollectionView变化的界限,因此我没有体验到视网膜和非视网膜之间的任何差异。)

我最终根据屏幕尺寸设置了sectionInsetand 。headerReferenceSize我尝试了大约 50 种组合,直到找到不再出现问题并且布局在视觉上可以接受的值。很难找到适用于两种屏幕尺寸的值。

总而言之,我只能建议您使用这些值,在不同的屏幕尺寸上检查这些值,并希望 Apple 能解决这个问题。

于 2012-10-29T17:50:25.860 回答
3

我刚刚遇到了类似的问题,即在iOS 10上 UICollectionView 滚动后单元格消失(在 iOS 6-9 上没有问题)。

UICollectionViewFlowLayout 的子类化和覆盖方法 layoutAttributesForElementsInRect: 在我的情况下不起作用。

解决方案很简单。目前我使用 UICollectionViewFlowLayout 的一个实例并设置itemSize 和estimatedItemSize(我之前没有使用estimatedItemSize )并将其设置为一些非零大小。实际大小在 collectionView:layout:sizeForItemAtIndexPath: 方法中计算。

此外,我已经从 layoutSubviews 中删除了对 invalidateLayout 方法的调用,以避免不必要的重新加载。

于 2016-10-31T09:44:58.140 回答
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;
}

这是正确保存 UICollectionViewLayoutAttributes 的 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
于 2015-06-19T00:32:35.490 回答
2

以上答案对我不起作用,但下载图片后,我替换了

[self.yourCollectionView reloadData]

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

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

于 2015-06-26T18:40:23.097 回答
0

prepare()这可能有点晚了,但如果可能的话,请确保您正在设置属性。

我的问题是单元格正在布局,然后在layoutAttributesForElements. 当新细胞进入视野时,这会导致闪烁效果。

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

于 2018-05-04T21:37:17.907 回答
0

Swift 5 版本的Nick Snyder回答:

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]
    }
}
于 2021-09-28T04:47:54.463 回答