web-dev-qa-db-ja.com

UICollectionView flowLayoutがセルを正しくラップしない

FLowLayoutを持つUICollectionViewがあります。ほとんどの場合は期待どおりに動作しますが、時々セルの1つが適切に折り返されません。たとえば、実際に2番目の行の末尾にある場合、3番目の行の最初の「列」にあるはずのセルで、その場所に空のスペースがあるだけです(下の図を参照)。このルージュセルを見ることができるのは左側のみで(残りは切り取られています)、本来あるべき場所は空です。

これは一貫して発生しません。常に同じ行であるとは限りません。それが起こったら、上にスクロールしてから元に戻すことができ、セルは自動的に修正されます。または、セルを押して(プッシュを使用して次のビューに移動します)ポップポップすると、セルが間違った位置に表示され、正しい位置にジャンプします。

スクロール速度により、問題の再現が容易になるようです。ゆっくりスクロールしても、時々間違った位置にあるセルを見ることができますが、すぐに正しい位置にジャンプします。

問題は、セクションのインセットを追加したときに始まりました。以前は、コレクションの境界に対してセルがほとんどフラッシュされていました(ほとんど、または挿入されていません)が、問題に気付きませんでした。しかし、これはコレクションビューの左右が空であることを意味していました。すなわち、スクロールできませんでした。また、スクロールバーは右側にフラッシュされませんでした。

シミュレーターとiPad 3の両方で問題を発生させることができます。

左と右のセクションのインセットが原因で問題が発生していると思います...しかし、値が間違っていれば、動作に一貫性があると考えられます。これはAppleのバグなのだろうか?またはおそらく、これはインセットの蓄積または類似のものによるものです。

Illustration of problem and settings


フォローアップ:私は2年以上も問題なく この答え を使用していますその答えに穴があるかどうか疑問に思う-私はまだ見つけていません)。よくやったニック。

77
lindon fox

UICollectionViewFlowLayoutのlayoutAttributesForElementsInRectの実装にはバグがあり、セクションインセットが関係する特定の場合に単一セルのTWO属性オブジェクトを返します。返された属性オブジェクトの1つは無効(コレクションビューの境界外)で、もう1つは有効です。以下は、コレクションビューの境界外のセルを除外することで問題を修正する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 を参照してください。

他の答えはshouldInvalidateLayoutForBoundsChangeからYESを返すことを示唆していますが、これは不必要な再計算を引き起こし、問題を完全には解決しません。

私の解決策はバグを完全に解決し、Appleが根本原因を修正するときに問題を引き起こさないはずです。

92
Nick Snyder

iPhoneアプリケーションでも同様の問題を発見しました。 Apple devフォーラムを検索すると、私の場合はうまくいき、おそらくあなたの場合もそうなるこの適切な解決策がもたらされました。

UICollectionViewFlowLayoutをサブクラス化し、shouldInvalidateLayoutForBoundsChangeをオーバーライドしてYESを返します。

//.h
@interface MainLayout : UICollectionViewFlowLayout
@end

そして

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

これをコレクションビューを所有するviewControllerに入れます

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

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

この問題は、マージン用のインセットを含む基本的なグリッドビューレイアウトでも発生しました。これまでに行った限定的なデバッグは、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を設定し、それから左右のインセットも削除しましたが、うまくいくようです。ヘッダービューを所定の位置にとどめるには、ヘッダービューのlayoutAttributesを返すタスクが割り当てられたレイアウトメソッドでフレームシフトとサイズ変更を実装する必要があります。

5
monowerker

Appleにバグレポートを追加しました。私にとってうまくいくのは、ボトムセクションのインセットをトップインセットよりも小さい値に設定することです。

4
DrMickeyLauer

iOS 1(iOS 6-9では問題ありません)でUICollectionViewスクロールした後、セルが消えるという同様の問題が発生しました。

私の場合、UICollectionViewFlowLayoutのサブクラス化とメソッドlayoutAttributesForElementsInRect:のオーバーライドは機能しません。

解決策は簡単でした。現在、UICollectionViewFlowLayoutのインスタンスを使用して、itemSizeとtimatedItemSizeの両方(以前はtimatedItemSizeを使用しませんでした)を設定し、ゼロ以外のサイズに設定します。実際のサイズは、collectionView:layout:sizeForItemAtIndexPath:メソッドで計算しています。

また、不要なリロードを避けるために、layoutSubviewsからinvalidateLayoutメソッドの呼び出しを削除しました。

3
Andrey Seredkin

IPhoneでUICollectionViewFlowLayoutを使用して同じセル置換の問題を経験していたので、あなたの投稿を見つけてうれしかったです。 iPadで問題が発生していることは知っていますが、UICollectionViewの一般的な問題だと思うので、これを投稿しています。だからここに私が見つけたものがあります。

sectionInsetがその問題に関連していることを確認できます。それに加えて、headerReferenceSizeはセルが置き換えられるかどうかにも影響します。 (Originの計算に必要なので、これは理にかなっています。)

残念ながら、異なる画面サイズでさえ考慮に入れる必要があります。これら2つのプロパティの値をいじってみると、特定の構成が両方(3.5 "と4")、なし、または画面サイズの1つのみで機能することがわかりました。通常、それらのどれも。 (UICollectionViewの境界が変化するため、これも理にかなっています。したがって、網膜と非網膜の間に差異はありませんでした。)

画面サイズに応じてsectionInsetheaderReferenceSizeを設定することになりました。問題が発生しなくなり、レイアウトが視覚的に許容できる値が見つかるまで、約50の組み合わせを試しました。両方の画面サイズで機能する値を見つけることは非常に困難です。

要約すると、値をいじって、さまざまな画面サイズでこれらを確認し、Appleがこの問題を解決することを願っています。

3
Masa

上記の答えは私にはうまくいきませんが、画像をダウンロードした後、私は置き換えました

[self.yourCollectionView reloadData]

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

更新してすべてのセルを正しく表示できるようにするには、試してみてください。

2
Yao Li

同様の問題が発生しましたが、非常に異なる解決策が見つかりました。

水平スクロールでUICollectionViewFlowLayoutのカスタム実装を使用しています。また、各セルにカスタムフレームの場所を作成しています。

私が抱えていた問題は、[super layoutAttributesForElementsInRect:rect]が実際に画面に表示されるべきUICollectionViewLayoutAttributesのすべてを返していなかったことです。 [self.collectionView reloadData]を呼び出すと、一部のセルが突然非表示に設定されます。

私がやったことは、これまで見てきたすべてのUICollectionViewLayoutAttributesをキャッシュしたNSMutableDictionaryを作成し、表示する必要があるとわかっている項目を含めることでした。

- (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
2
Endama

これは少し遅いかもしれませんが、可能であればprepare()で属性を設定していることを確認してください。

私の問題は、セルがレイアウトされ、layoutAttributesForElementsで更新されることでした。これにより、新しいセルが表示されるとフリッカー効果が生じました。

すべての属性ロジックをprepareに移動してから、それらをUICollectionViewCell.apply()に設定すると、ちらつきがなくなり、バターの滑らかなセル表示????が作成されます。

0
Michael