UICollectionView flowLayout nie zawija poprawnie komórek

79

Mam plik UICollectionViewz FLowLayout. Przez większość czasu będzie działać tak, jak się spodziewam, ale od czasu do czasu jedna z komórek nie owija się prawidłowo. Na przykład komórka, która powinna znajdować się w pierwszej „kolumnie” trzeciego wiersza, jeśli faktycznie znajduje się na końcu drugiego wiersza, aw miejscu jej umieszczenia jest tylko puste miejsce (patrz diagram poniżej). Wszystko, co widzisz w tej komórce różu, to lewa strona (reszta jest odcięta), a miejsce, w którym powinna być, jest puste.

Nie dzieje się to konsekwentnie; nie zawsze jest to ten sam wiersz. Kiedy to się stanie, mogę przewinąć w górę i z powrotem, a komórka się naprawi. Lub, gdy naciskam komórkę (która przenosi mnie do następnego widoku poprzez naciśnięcie), a następnie cofam, zobaczę komórkę w niewłaściwej pozycji, a następnie przeskoczy do właściwej pozycji.

Wydaje się, że szybkość przewijania ułatwia odtworzenie problemu. Kiedy przewijam powoli, nadal widzę od czasu do czasu komórkę w niewłaściwej pozycji, ale wtedy od razu przeskoczy do właściwej pozycji.

Problem zaczął się, gdy dodałem wstawki sekcji. Wcześniej komórki były prawie równe z granicami kolekcji (małe wstawki lub brak wstawek) i nie zauważyłem problemu. Ale to oznaczało, że prawa i lewa część widoku kolekcji były puste. To znaczy, nie można przewijać. Ponadto pasek przewijania nie był wyrównany w prawo.

Mogę rozwiązać problem zarówno w symulatorze, jak i na iPadzie 3.

Wydaje mi się, że problem występuje z powodu wstawek lewej i prawej sekcji ... Ale jeśli wartość jest nieprawidłowa, spodziewałbym się, że zachowanie będzie spójne. Zastanawiam się, czy to może być błąd w Apple? A może jest to spowodowane nagromadzeniem wypustek lub czymś podobnym.

Ilustracja problemu i ustawień


Kontynuacja : od ponad 2 lat korzystam z poniższej odpowiedzi Nicka bez problemu (na wypadek, gdyby ludzie zastanawiali się, czy w tej odpowiedzi są jakieś dziury - jeszcze żadnej nie znalazłem). Dobra robota, Nick.

lindon fox
źródło

Odpowiedzi:

94

Wystąpił błąd w implementacji layoutAttributesForElementsInRect w UICollectionViewFlowLayout, który powoduje, że zwraca DWA obiekty atrybutów dla pojedynczej komórki w niektórych przypadkach dotyczących wstawek sekcji. Jeden ze zwróconych obiektów atrybutów jest nieprawidłowy (poza granicami widoku kolekcji), a drugi jest prawidłowy. Poniżej znajduje się podklasa UICollectionViewFlowLayout, która rozwiązuje problem, wykluczając komórki poza granicami widoku kolekcji.

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

Zobacz to .

Inne odpowiedzi sugerują zwrócenie TAK z shouldInvalidateLayoutForBoundsChange, ale powoduje to niepotrzebne ponowne obliczenia i nawet nie rozwiązuje całkowicie problemu.

Moje rozwiązanie całkowicie rozwiązuje błąd i nie powinno powodować żadnych problemów, gdy Apple naprawi główną przyczynę.

Nick Snyder
źródło
5
FYI, ten błąd pojawia się również podczas przewijania w poziomie. Zamiana x na y i szerokość na wysokość sprawia, że ​​ta łatka działa.
Patrick Tescher,
Dzięki! Właśnie zacząłem bawić się collectionView (jeśli chcesz spamować Apple w tej sprawie, oto odnośnik do rdar openradar.appspot.com/12433891 )
Vinzzz
1
@richarddas Nie, nie chcesz sprawdzać, czy prostokąty się przecinają. W rzeczywistości wszystkie komórki (prawidłowe lub nieprawidłowe) będą przecinać prostopadłe granice widoku kolekcji. Chcesz sprawdzić, czy jakakolwiek część prostego nie wykracza poza granice, co robi mój kod.
Nick Snyder
2
@Rpranata Od iOS 7.1 ten błąd nie został naprawiony. Westchnienie.
nonamelive
2
Może używam tego źle, ale na iOS 8.3 w Swift powoduje to, że podglądy po prawej stronie, które były ucięte, w ogóle się nie pojawiają. Ktoś jeszcze?
sudo
8

Umieść to w viewController, który jest właścicielem widoku kolekcji

- (void)viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];
    [self.collectionView.collectionViewLayout invalidateLayout];
}
Peter Lapisu
źródło
Gdzie to umieściłeś?
fatuhoku
Into the viewController, który jest właścicielem widoku kolekcji
Peter Lapisu
3
Mój problem polegał na tym, że komórki całkowicie zniknęły. To rozwiązanie pomogło - jednak powoduje to niepotrzebne przeładowania. Nadal działa teraz .. dzięki!
pawi
1
powoduje nieskończoną pętlę, jeśli wywołujesz mnie z
viewController
Jak zauważył DHennessy13 , to obecne rozwiązanie jest dobre, ale może być niedoskonałe, ponieważ spowoduje unieważnienie układu podczas obracania ekranu (iw większości przypadków nie powinno). Poprawą mogłoby być ustawienie flagi invalidateLayouttylko raz.
Cœur
7

Odkryłem podobne problemy w mojej aplikacji na iPhone'a. Przeszukanie forum deweloperów Apple przyniosło mi to odpowiednie rozwiązanie, które zadziałało w moim przypadku i prawdopodobnie będzie również w twoim:

Podklasa UICollectionViewFlowLayouti przesłonięcie, shouldInvalidateLayoutForBoundsChangeaby zwrócić YES.

//.h
@interface MainLayout : UICollectionViewFlowLayout
@end

i

//.m
#import "MainLayout.h"
@implementation MainLayout
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
    return YES;
}
@end
xxtesaxx
źródło
To tylko częściowo rozwiązuje problem, który mam z tym problemem. Komórka rzeczywiście trafia we właściwe miejsce, gdy pojawia się wiersz. Jednak tuż przed pojawieniem się komórki nadal pojawia się komórka z boku.
Daniel Wood
Ostrożnie - wykonanie tej czynności spowoduje uruchomienie układu przy każdym przewijaniu. Może to poważnie wpłynąć na wydajność.
fatuhoku
7

Szybka wersja odpowiedzi Nicka Snydera:

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 }
    }
}
Patrick Pijnappel
źródło
1
to całkowicie sprawiło, że CollectionViewCell zniknęła ... Jakieś inne możliwe rozwiązanie?
Giggs,
5

Miałem również ten problem w przypadku podstawowego układu widoku siatki z wstawkami na marginesy. Ograniczone debugowanie, które zrobiłem na razie, polega na implementacji - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rectw mojej podklasie UICollectionViewFlowLayout i przez rejestrowanie tego, co zwraca implementacja superklasy, co wyraźnie pokazuje problem.

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

Po wdrożeniu - (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPathwidzę również, że wydaje się zwracać niewłaściwe wartości dla itemIndexPath.item == 30, co stanowi 10 czynnik liczby komórek w moim widoku siatki w wierszu, nie jestem pewien, czy to ma znaczenie.

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

Z braku czasu na dalsze debugowanie, obejście, które zrobiłem, polega na zmniejszeniu szerokości moich podglądów kolekcji o kwotę równą lewemu i prawemu marginesowi. Mam nagłówek, który nadal wymaga pełnej szerokości, więc ustawiłem clipsToBounds = NO w moim widoku kolekcji, a następnie usunąłem z niego lewe i prawe wstawki. Wygląda na to, że działa. Aby widok nagłówka pozostał na miejscu, należy zaimplementować przesuwanie klatek i zmianę rozmiaru w metodach układu, które mają za zadanie zwracanie atrybutów layout dla widoku nagłówka.

monowerker
źródło
Dzięki za dodatkowe informacje @monowerker. Myślę, że mój problem zaczął się, kiedy dodałem wstawki (dodałem to do pytania). Spróbuję twoich metod debugowania i zobaczę, czy coś mi powie. Mogę też spróbować twojej pracy.
lindon fox
Najprawdopodobniej jest to błąd w UICFL / UICL, spróbuję złożyć radar, kiedy będę miał czas, tutaj jest dyskusja z niektórymi numerami rdar, do których możesz się odwołać. twitter.com/steipete/status/258323913279410177
monowerker
4

Dodałem raport o błędzie do Apple. To, co działa dla mnie, to ustawienie dolnej sekcjiInset na wartość mniejszą niż górna wstawka.

DrMickeyLauer
źródło
3

Miałem ten sam problem ze zubożeniem komórki na iPhonie przy użyciu UICollectionViewFlowLayouta, więc byłem zadowolony ze znalezienia twojego postu. Wiem, że masz problem z iPadem, ale publikuję to, ponieważ myślę, że jest to ogólny problem z UICollectionView. Więc oto, czego się dowiedziałem.

Mogę potwierdzić, że sectionInsetjest to istotne dla tego problemu. Poza tym headerReferenceSizema również wpływ na to, czy komórka jest wyczerpana, czy nie. (Ma to sens, ponieważ jest potrzebne do obliczenia pochodzenia).

Niestety, należy wziąć pod uwagę nawet różne rozmiary ekranu. Podczas zabawy z wartościami tych dwóch właściwości zauważyłem, że pewna konfiguracja działała albo na obu (3,5 "i 4"), na żadnej lub tylko na jednym z rozmiarów ekranu. Zwykle żaden z nich. (Ma to również sens, ponieważ graniceUICollectionView zmian, dlatego nie doświadczyłem żadnej dysproporcji między siatkówką i bez siatkówki).

Skończyło się na ustawieniu sectionInseti w headerReferenceSizezależności od rozmiaru ekranu. Próbowałem około 50 kombinacji, aż znalazłem wartości, pod którymi problem już nie występował, a układ był wizualnie akceptowalny. Bardzo trudno jest znaleźć wartości, które działają na obu rozmiarach ekranu.

Podsumowując, mogę po prostu polecić zabawę z wartościami, sprawdzenie ich na różnych rozmiarach ekranu i mam nadzieję, że Apple rozwiąże ten problem.

Masa
źródło
3

Właśnie napotkałem podobny problem ze znikaniem komórek po przewijaniu UICollectionView na iOS 10 (nie mam problemów na iOS 6-9).

Podklasa UICollectionViewFlowLayout i przesłanianie layoutAttributesForElementsInRect metody: nie działa w moim przypadku.

Rozwiązanie było dość proste. Obecnie używam instancji UICollectionViewFlowLayout i ustawiam zarówno itemSize, jak i aestItemSize (wcześniej nie używałem oszacowanego rozmiaru) i ustawiam go na niezerowy rozmiar. Rzeczywisty rozmiar jest obliczany w collectionView: layout: sizeForItemAtIndexPath: metoda.

Usunąłem również wywołanie metody invalidateLayout z layoutSubviews, aby uniknąć niepotrzebnych przeładowań.

Andrey Seredkin
źródło
gdzie ustawiono itemsize i aestItemsize w uicollectionviewflowlayout?
Garrett Cox,
UICollectionViewFlowLayout * flowLayout = [[UICollectionViewFlowLayout Alokacja] init]; [flowLayout setItemSize: CGSizeMake (200, 200)]; [flowLayout setEstimatedItemSize: CGSizeMake (200, 200)]; self.collectionView = [[UICollectionView assign] initWithFrame: CGRectZero collectionViewLayout: flowLayout];
Andrey Seredkin
Ustawienie oszacowanego
rozmiaru
2

Właśnie doświadczyłem podobnego problemu, ale znalazłem zupełnie inne rozwiązanie.

Używam niestandardowej implementacji UICollectionViewFlowLayout z przewijaniem poziomym. Tworzę również niestandardowe lokalizacje ramek dla każdej komórki.

Problem, jaki miałem, polegał na tym, że [super layoutAttributesForElementsInRect: rect] w rzeczywistości nie zwracał wszystkich atrybutów UICollectionViewLayoutAttributes, które powinny być wyświetlane na ekranie. W przypadku wywołań [self.collectionView reloadData] niektóre komórki byłyby nagle ustawiane jako ukryte.

Skończyło się na utworzeniu NSMutableDictionary, który buforował wszystkie atrybuty UICollectionViewLayoutAttributes, które widziałem do tej pory, a następnie zawierał wszystkie elementy, o których wiem, że powinny zostać wyświetlone.

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

Oto kategoria dla NSMutableDictionary, w której UICollectionViewLayoutAttributes są zapisywane poprawnie.

#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
Endama
źródło
Używam też przewijania w poziomie, udaje mi się rozwiązać problem za pomocą twojego rozwiązania, ale po wykonaniu przejścia do innego widoku i powrocie rozmiar treści wydawał się zły, gdy są dodatkowe elementy, które nie dzielą się równo na kolumny.
morph85
Znalazłem rozwiązanie, aby naprawić problem polegający na tym, że komórka była ukrywana po wykonaniu przejścia i powrocie do widoku kolekcji. Staraj się nie ustawiaćimateItemSize w collectionViewFlowLayout; ustaw itemSize bezpośrednio.
morph85
2

Powyższe odpowiedzi u mnie nie działają, ale po pobraniu zdjęć wymieniłem

[self.yourCollectionView reloadData]

z

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

aby odświeżyć i poprawnie wyświetlać wszystkie komórki, możesz spróbować.

Yao Li
źródło
0

To może być trochę za późno, ale upewnij się, że ustawiasz atrybuty, prepare()jeśli to możliwe.

Mój problem polegał na tym, że komórki układały się, a następnie pobierałem aktualizację layoutAttributesForElements. Spowodowało to efekt migotania, gdy pojawiły się nowe komórki.

Przeniesienie całej logiki atrybutów do prepare, a następnie ustawienie ich w UICollectionViewCell.apply()niej wyeliminowało migotanie i utworzyło gładką komórkę wyświetlającą masło 😊

Michael
źródło