UICollectionView流式布局未正确包装单元格

79
我有一个带有FlowLayout的UICollectionView。大部分时间它都能正常工作,但是偶尔会出现一个单元格无法正确换行的情况。例如,在第三行的第一列应该出现的单元格实际上却在第二行后面,并且空着的地方应该是它的位置(如下图所示)。你只能看到这个单元格的左侧(其余部分被剪切了),并且应该出现的位置为空白。

这种情况不是总是发生;每次发生的行数都不同。一旦出现这种情况,我可以向上滚动,然后再返回,此时单元格就会自动修复。或者,当我点击该单元格(通过推送进入下一个视图)然后再按“返回”键时,我会看到该单元格处于错误位置,然后它会跳转到正确的位置。

滚动速度似乎会使问题更容易重现。当我慢慢滚动时,偶尔仍然能看到单元格在错误位置,但随即它就会立即跳回正确位置。

问题始于我添加了段落插入。以前,我的单元格几乎贴在集合边界上(很少或没有插入),我没有注意到问题。但这意味着集合视图的左右两侧是空的。即,无法滚动。另外,滚动条没有靠在右边。

我可以在模拟器和iPad 3上重现此问题。

我想问题是由于左右段落插入引起的...但如果值是错误的,那么我期望行为应该是一致的。我不知道这是否可能是Apple的一个错误?或者这可能是由于插入或类似的东西累计而导致的。

Illustration of problem and settings

跟进:我已经使用Nick在下面提供的答案两年多了,没有任何问题(如果有人想知道那个答案是否存在漏洞 - 我还没有发现)。干得好,Nick。

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,但这会导致不必要的重新计算,并且甚至不能完全解决问题。

我的解决方案完全解决了该bug,当苹果修复根本原因时,不应该出现任何问题。


5
FYI,这个 bug 出现在水平滚动时也会出现。将 x 替换为 y,将 width 替换为 height 可以使此补丁工作。 - Patrick Tescher
与其检查矩形的各个参数,你可以直接使用 CGRectIntersectsRect(attribute.frame, rect) - cleverbit
1
@richarddas 不,你不想检查矩形是否相交。实际上,所有单元格(有效或无效)都将与集合视图的边界矩形相交。你想要检查矩形的任何部分是否超出了边界,这就是我的代码所做的。 - Nick Snyder
2
截至iOS 7.1,这个漏洞仍未被修复。唉。 - nonamelive
2
也许我使用不当,但在iOS 8.3中使用Swift,这会导致右侧的子视图不再出现。还有其他人遇到这个问题吗? - sudo
显示剩余8条评论

8
把这段代码放入拥有集合视图的viewController中。
- (void)viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];
    [self.collectionView.collectionViewLayout invalidateLayout];
}

你把它放在哪里? - fatuhoku
进入拥有集合视图的视图控制器。 - Peter Lapisu
3
我的问题是细胞完全消失了。这个解决方案有所帮助,但会导致不必要的重新加载。但现在它已经可以工作了,谢谢! - pawi
1
如果从视图控制器中调用它,会导致无限循环。 - Hofi
正如DHennessy13所指出的那样,当前的解决方案虽然不错,但在旋转屏幕时可能会无效化布局(对于大多数情况来说是不应该的)。一个改进的方法是设置一个标志,只有在必要时才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

这只是部分解决了我在这个问题上遇到的困难。当行出现时,该单元格确实会到达正确的位置。然而,在它出现之前,我仍然会看到一个单元格出现在侧面。 - Daniel Wood
注意——这样做将导致布局每次滚动时都被执行。这可能会严重影响性能。 - fatuhoku

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

2
这会完全使CollectionViewCell消失...还有其他可能的解决方案吗? - Giggs

5
我也遇到了这个问题,因为我在基本的网格视图布局中使用了插入余量。目前我进行了有限的调试,是通过在我的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;
}

由于没有更多的时间进行调试,我现在所做的解决方法是将我的集合视图宽度减少一个等于左右边距的数量。我有一个仍需要完整宽度的头部视图,因此我在我的集合视图上设置了 clipsToBounds = NO,并且还删除了其左右插入。看起来可以工作。为了让头部视图保持在原位,您需要在负责返回头部视图布局属性的布局方法中实现框架移位和大小调整。


感谢 @monowerker 提供的额外信息。我认为我的问题是添加插图后开始出现的(我已将其添加到问题中)。我将尝试您的调试方法,看看它是否会告诉我任何有用的信息。我可能也会尝试您的解决方法。 - lindon fox
这很可能是UICFL/UICL中的一个错误,我会尽力在有时间时提交一个radar,这里有一些带有rdar-numbers的讨论可供参考。 http://twitter.com/steipete/status/258323913279410177 - monowerker

4

我已向苹果提交了一个错误报告。对我来说有效的方法是将底部sectionInset设置为小于顶部inset的值。


3

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

在我的情况下,通过子类化 UICollectionViewFlowLayout 并覆盖方法 layoutAttributesForElementsInRect: 是无效的。

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

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


在UICollectionViewFlowLayout中,itemSize和estimatedItemSize属性是在哪里设置的? - Garrett Cox
UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init]; [flowLayout setItemSize:CGSizeMake(200, 200)]; [flowLayout setEstimatedItemSize:CGSizeMake(200, 200)]; self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:flowLayout]; - Andrey Seredkin
设置 estimatedItemSize 对我有用。 - wmurmann

3
我在iPhone上使用UICollectionViewFlowLayout时遇到了同样的单元格移位问题,因此我很高兴找到了你的帖子。 我知道您在iPad上遇到了这个问题,但我发布这篇文章是因为我认为这是一个通用的 UICollectionView 问题。 所以这是我发现的。

我可以确认 sectionInset 与该问题有关。 除此之外, headerReferenceSize 也会影响单元格是否被排除。(这很合理,因为它需要计算起点。)

不幸的是,甚至还必须考虑不同的屏幕大小。 当尝试这两个属性值时,我发现特定的配置适用于两者(3.5“和4”),或全部都不适用,或仅适用于其中一个屏幕大小。 通常没有一个。 (这也很合理,因为 UICollectionView 的范围会改变,因此我没有在视网膜和非视网膜之间经历任何差异。)

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

因此,总结一下,我只能建议您尝试调整这些值,检查不同屏幕大小上的效果,并希望Apple解决此问题。


2
上述答案对我无效,但在下载图像后,我替换了

[self.yourCollectionView reloadData]

使用

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

如果您希望刷新并正确显示所有单元格,可以尝试此操作。


2
我刚遇到过一个类似的问题,但找到了一种非常不同的解决方案。
我正在使用自定义的UICollectionViewFlowLayout来进行水平滚动。我还为每个cell创建了自定义的frame位置。
我的问题是[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

我也在使用水平滚动,我使用了您的解决方法来修复问题,但在执行segue跳转到另一个视图并返回后,当有额外的项目无法均匀地分成列时,内容大小似乎是错误的。 - morph85
我找到了解决方案,可以修复在执行segue并返回到集合视图后单元格被隐藏的问题。尝试不要在collectionViewFlowLayout中设置estimatedItemSize;直接设置itemSize即可。 - morph85

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接