Wyrównaj komórki do lewej w UICollectionView

98

W moim projekcie używam UICollectionView, w którym w wierszu znajduje się wiele komórek o różnych szerokościach. Według: https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html

rozprowadza komórki wzdłuż linii z równym wypełnieniem. Dzieje się to zgodnie z oczekiwaniami, z wyjątkiem tego, że chcę je wyrównać i na stałe zakodować szerokość dopełnienia.

Wydaje mi się, że muszę podklasę UICollectionViewFlowLayout, jednak po przeczytaniu niektórych samouczków itp. W Internecie po prostu nie wydaje się, aby rozumieć, jak to działa.

Damien
źródło

Odpowiedzi:

175

Pozostałe rozwiązania w tym wątku nie działają poprawnie, gdy linia składa się tylko z 1 pozycji lub jest zbyt skomplikowana.

Bazując na przykładzie podanym przez Ryana, zmieniłem kod, aby wykrywał nową linię, sprawdzając pozycję Y nowego elementu. Bardzo proste i szybkie w wykonaniu.

Szybki:

class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElements(in: rect)

        var leftMargin = sectionInset.left
        var maxY: CGFloat = -1.0
        attributes?.forEach { layoutAttribute in
            if layoutAttribute.frame.origin.y >= maxY {
                leftMargin = sectionInset.left
            }

            layoutAttribute.frame.origin.x = leftMargin

            leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
            maxY = max(layoutAttribute.frame.maxY , maxY)
        }

        return attributes
    }
}

Jeśli chcesz, aby dodatkowe widoki zachowały swój rozmiar, dodaj następujące informacje u góry zamknięcia w forEachwywołaniu:

guard layoutAttribute.representedElementCategory == .cell else {
    return
}

Cel C:

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSArray *attributes = [super layoutAttributesForElementsInRect:rect];

    CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use.
    CGFloat maxY = -1.0f;

    //this loop assumes attributes are in IndexPath order
    for (UICollectionViewLayoutAttributes *attribute in attributes) {
        if (attribute.frame.origin.y >= maxY) {
            leftMargin = self.sectionInset.left;
        }

        attribute.frame = CGRectMake(leftMargin, attribute.frame.origin.y, attribute.frame.size.width, attribute.frame.size.height);

        leftMargin += attribute.frame.size.width + self.minimumInteritemSpacing;
        maxY = MAX(CGRectGetMaxY(attribute.frame), maxY);
    }

    return attributes;
}
Angel G. Olloqui
źródło
4
Dzięki! let attributes = super.layoutAttributesForElementsInRect(rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }
Otrzymałem
2
Dla każdego, kto mógł natknąć się na to, szukając wersji wyrównanej do środka, tak jak ja, opublikowałem zmodyfikowaną wersję odpowiedzi Anioła tutaj: stackoverflow.com/a/38254368/2845237
Alex Koshy
Wypróbowałem to rozwiązanie, ale niektóre komórki znajdujące się po prawej stronie widoku kolekcji przekraczają granice widoku kolekcji. Czy ktoś inny to spotkał?
Happiehappie
@Happiehappie tak, ja też mam takie zachowanie. jakaś poprawka?
John Baum
Wypróbowałem to rozwiązanie, ale nie mogłem skojarzyć układu z collectionView w panelu Interface Builder Utilies. Skończyło się na tym, że zrobiłem to za pomocą kodu. Jakaś wskazówka, dlaczego?
acrespo
64

Odpowiedzi na to pytanie zawierają wiele wspaniałych pomysłów. Jednak większość z nich ma pewne wady:

  • Rozwiązania, które nie sprawdzają wartości y komórki, działają tylko w przypadku układów jednowierszowych . Niepowodzenie w przypadku układów widoku kolekcji z wieloma wierszami.
  • Rozwiązania, które sprawdzają wartość y, takie jak odpowiedź Angel García Olloqui, działają tylko wtedy, gdy wszystkie komórki mają tę samą wysokość . Zawodzą w przypadku komórek o zmiennej wysokości.
  • Większość rozwiązań tylko zastępuje tę layoutAttributesForElements(in rect: CGRect)funkcję. Nie zastępują layoutAttributesForItem(at indexPath: IndexPath). Jest to problem, ponieważ widok kolekcji okresowo wywołuje tę ostatnią funkcję w celu pobrania atrybutów układu dla określonej ścieżki indeksu. Jeśli nie zwrócisz odpowiednich atrybutów z tej funkcji, prawdopodobnie napotkasz wszelkiego rodzaju wizualne błędy, np. Podczas wstawiania i usuwania animacji komórek lub podczas używania samoczynnie zmieniających się rozmiarów komórek przez ustawienie układu widoku kolekcji estimatedItemSize. Dokumentacja Apple stwierdza:

    Oczekuje się, że każdy niestandardowy obiekt układu będzie implementował tę layoutAttributesForItemAtIndexPath:metodę.

  • Wiele rozwiązań przyjmuje również założenia dotyczące rect parametru przekazywanego do layoutAttributesForElements(in rect: CGRect)funkcji. Na przykład wiele z nich opiera się na założeniu, że rectzawsze zaczyna się na początku nowej linii, co niekoniecznie ma miejsce.

Więc innymi słowy:

Większość rozwiązań sugerowanych na tej stronie działa w określonych aplikacjach, ale nie działają one zgodnie z oczekiwaniami w każdej sytuacji.


AlignedCollectionViewFlowLayout

Aby rozwiązać te problemy, stworzyłem UICollectionViewFlowLayoutpodklasę, która kieruje się podobną ideą, jak sugerowali Matt i Chris Wagner w odpowiedziach na podobne pytanie. Może wyrównać komórki

⬅︎ lewo :

Układ wyrównany do lewej

lub w ➡︎ prawo :

Układ wyrównany do prawej

a dodatkowo oferuje opcje pionowego wyrównania komórek w odpowiednich wierszach (w przypadku, gdy różnią się wysokością).

Możesz go po prostu pobrać tutaj:

https://github.com/mischa-hildebrand/AlignedCollectionViewFlowLayout

Użycie jest proste i wyjaśnione w pliku README. Zasadniczo tworzysz wystąpienie AlignedCollectionViewFlowLayout, określasz żądane wyrównanie i przypisujesz je do collectionViewLayoutwłaściwości widoku kolekcji :

 let alignedFlowLayout = AlignedCollectionViewFlowLayout(horizontalAlignment: .left, 
                                                         verticalAlignment: .top)

 yourCollectionView.collectionViewLayout = alignedFlowLayout

(Jest również dostępny na Cocoapods .)


Jak to działa (dla komórek wyrównanych do lewej):

Tutaj chodzi o poleganie wyłącznie na layoutAttributesForItem(at indexPath: IndexPath)funkcji . W tym layoutAttributesForElements(in rect: CGRect)po prostu pobieramy ścieżki indeksu wszystkich komórek w obrębie, recta następnie wywołujemy pierwszą funkcję dla każdej ścieżki indeksu, aby pobrać prawidłowe ramki:

override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

    // We may not change the original layout attributes 
    // or UICollectionViewFlowLayout might complain.
    let layoutAttributesObjects = copy(super.layoutAttributesForElements(in: rect))

    layoutAttributesObjects?.forEach({ (layoutAttributes) in
        if layoutAttributes.representedElementCategory == .cell { // Do not modify header views etc.
            let indexPath = layoutAttributes.indexPath
            // Retrieve the correct frame from layoutAttributesForItem(at: indexPath):
            if let newFrame = layoutAttributesForItem(at: indexPath)?.frame {
                layoutAttributes.frame = newFrame
            }
        }
    })

    return layoutAttributesObjects
}

( copy()Funkcja po prostu tworzy głęboką kopię wszystkich atrybutów układu w tablicy. Możesz zajrzeć do kodu źródłowego pod kątem jego implementacji).

Więc teraz jedyne, co musimy zrobić, to layoutAttributesForItem(at indexPath: IndexPath)poprawnie zaimplementować tę funkcję. Superklasa UICollectionViewFlowLayoutjuż umieszcza właściwą liczbę komórek w każdym wierszu, więc musimy tylko przesunąć je w lewo w odpowiednim wierszu. Trudność polega na obliczeniu ilości miejsca potrzebnego do przesunięcia każdej komórki w lewo.

Ponieważ chcemy mieć stały odstęp między komórkami, podstawową ideą jest po prostu założenie, że poprzednia komórka (komórka na lewo od komórki, która jest obecnie ułożona) jest już prawidłowo ustawiona. Następnie musimy tylko dodać odstępy między komórkami do maxXwartości ramki poprzedniej komórki i to jestorigin.x wartość ramki bieżącej komórki.

Teraz musimy tylko wiedzieć, kiedy dotarliśmy do początku wiersza, aby nie wyrównać komórki do komórki w poprzednim wierszu. (Spowodowałoby to nie tylko nieprawidłowy układ, ale również byłoby bardzo opóźnione.) Musimy więc mieć kotwicę rekurencji. Podejście, którego używam do znalezienia tej kotwicy rekurencji, jest następujące:

Aby dowiedzieć się, czy komórka o indeksie i znajduje się w tym samym wierszu, co komórka o indeksie i-1 ...

 +---------+----------------------------------------------------------------+---------+
 |         |                                                                |         |
 |         |     +------------+                                             |         |
 |         |     |            |                                             |         |
 | section |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| section |
 |  inset  |     |intersection|        |                     |   line rect  |  inset  |
 |         |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -|         |
 | (left)  |     |            |             current item                    | (right) |
 |         |     +------------+                                             |         |
 |         |     previous item                                              |         |
 +---------+----------------------------------------------------------------+---------+

... „Rysuję” prostokąt wokół bieżącej komórki i rozciągam go na szerokość całego widoku kolekcji. Ponieważ UICollectionViewFlowLayoutcentra wszystkie komórki w pionie, każda komórka w tej samej linii musi przecinać się z tym prostokątem.

W ten sposób po prostu sprawdzam, czy komórka o indeksie i-1 przecina się z tym prostokątem linii utworzonym z komórki o indeksie i .

  • Jeśli przecina się, komórka o indeksie i nie jest ostatnią komórką w linii z lewej strony.
    → Pobierz ramkę z poprzedniej komórki (z indeksem i − 1 ) i przenieś do niej bieżącą komórkę.

  • Jeśli się nie przecina, komórka z indeksem i jest ostatnią komórką w linii z lewej strony.
    → Przenieś komórkę do lewej krawędzi widoku kolekcji (bez zmiany jej pozycji w pionie).

Nie będę tutaj umieszczał faktycznej implementacji layoutAttributesForItem(at indexPath: IndexPath)funkcji, ponieważ uważam, że najważniejsze jest zrozumienie pomysłu i zawsze możesz sprawdzić moją implementację w kodzie źródłowym . (Jest to trochę bardziej skomplikowane niż wyjaśniono tutaj, ponieważ zezwalam również na .rightwyrównanie i różne opcje wyrównania w pionie. Jednak jest to ta sama idea).


Wow, myślę, że to najdłuższa odpowiedź, jaką kiedykolwiek napisałem w Stackoverflow. Mam nadzieję, że to pomoże. 😉

Mischa
źródło
Aplikuję w tym scenariuszu stackoverflow.com/questions/56349052/ ... ale to nie działa. czy możesz mi powiedzieć, jak mogę to osiągnąć
Nazmul Hasan
Nawet przy tym (który jest naprawdę niesamowitym projektem) miga w widoku kolekcji z przewijaniem pionowym moich aplikacji. Mam 20 komórek i przy pierwszym przewijaniu pierwsza komórka jest pusta, a wszystkie komórki są przesunięte o jedną pozycję w prawo. Po pełnym przewinięciu w górę iw dół wszystko układa się prawidłowo.
Parth Tamane
Świetny projekt! Byłoby wspaniale, gdyby zostało to włączone dla Kartaginy.
pjuzeliunas
30

Dzięki Swift 4.1 i iOS 11, w zależności od potrzeb, możesz wybrać jedną z 2 następujących kompletnych implementacji , aby rozwiązać problem.


# 1. Wyrównanie do lewej autorezowanie UICollectionViewCells

Poniższa implementacja pokazuje, jak używać UICollectionViewLayout znaków 's layoutAttributesForElements(in:), UICollectionViewFlowLayout' s estimatedItemSizei UILabel's preferredMaxLayoutWidth, aby wyrównać do lewej autorezację komórek w a UICollectionView:

CollectionViewController.swift

import UIKit

class CollectionViewController: UICollectionViewController {

    let array = ["1", "1 2", "1 2 3 4 5 6 7 8", "1 2 3 4 5 6 7 8 9 10 11", "1 2 3", "1 2 3 4", "1 2 3 4 5 6", "1 2 3 4 5 6 7 8 9 10", "1 2 3 4", "1 2 3 4 5 6 7", "1 2 3 4 5 6 7 8 9", "1", "1 2 3 4 5", "1", "1 2 3 4 5 6"]

    let columnLayout = FlowLayout(
        minimumInteritemSpacing: 10,
        minimumLineSpacing: 10,
        sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
    )

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView?.collectionViewLayout = columnLayout
        collectionView?.contentInsetAdjustmentBehavior = .always
        collectionView?.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return array.count
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
        cell.label.text = array[indexPath.row]
        return cell
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        collectionView?.collectionViewLayout.invalidateLayout()
        super.viewWillTransition(to: size, with: coordinator)
    }

}

FlowLayout.swift

import UIKit

class FlowLayout: UICollectionViewFlowLayout {

    required init(minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
        super.init()

        estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
        self.minimumInteritemSpacing = minimumInteritemSpacing
        self.minimumLineSpacing = minimumLineSpacing
        self.sectionInset = sectionInset
        sectionInsetReference = .fromSafeArea
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
        guard scrollDirection == .vertical else { return layoutAttributes }

        // Filter attributes to compute only cell attributes
        let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })

        // Group cell attributes by row (cells with same vertical center) and loop on those groups
        for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
            // Set the initial left inset
            var leftInset = sectionInset.left

            // Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
            for attribute in attributes {
                attribute.frame.origin.x = leftInset
                leftInset = attribute.frame.maxX + minimumInteritemSpacing
            }
        }

        return layoutAttributes
    }

}

CollectionViewCell.swift

import UIKit

class CollectionViewCell: UICollectionViewCell {

    let label = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)

        contentView.backgroundColor = .orange
        label.preferredMaxLayoutWidth = 120
        label.numberOfLines = 0

        contentView.addSubview(label)
        label.translatesAutoresizingMaskIntoConstraints = false
        contentView.layoutMarginsGuide.topAnchor.constraint(equalTo: label.topAnchor).isActive = true
        contentView.layoutMarginsGuide.leadingAnchor.constraint(equalTo: label.leadingAnchor).isActive = true
        contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: label.trailingAnchor).isActive = true
        contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: label.bottomAnchor).isActive = true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

Spodziewany wynik:

wprowadź opis obrazu tutaj


# 2. Wyrównanie do lewej UICollectionViewCellze stałym rozmiarem

Poniższa implementacja pokazuje, jak używać UICollectionViewLayoutplikówlayoutAttributesForElements(in:) i UICollectionViewFlowLayoutitemSizew celu wyrównania komórek do lewej strony ze wstępnie zdefiniowanym rozmiarem w plikuUICollectionView :

CollectionViewController.swift

import UIKit

class CollectionViewController: UICollectionViewController {

    let columnLayout = FlowLayout(
        itemSize: CGSize(width: 140, height: 140),
        minimumInteritemSpacing: 10,
        minimumLineSpacing: 10,
        sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
    )

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView?.collectionViewLayout = columnLayout
        collectionView?.contentInsetAdjustmentBehavior = .always
        collectionView?.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 7
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
        return cell
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        collectionView?.collectionViewLayout.invalidateLayout()
        super.viewWillTransition(to: size, with: coordinator)
    }

}

FlowLayout.swift

import UIKit

class FlowLayout: UICollectionViewFlowLayout {

    required init(itemSize: CGSize, minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
        super.init()

        self.itemSize = itemSize
        self.minimumInteritemSpacing = minimumInteritemSpacing
        self.minimumLineSpacing = minimumLineSpacing
        self.sectionInset = sectionInset
        sectionInsetReference = .fromSafeArea
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
        guard scrollDirection == .vertical else { return layoutAttributes }

        // Filter attributes to compute only cell attributes
        let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })

        // Group cell attributes by row (cells with same vertical center) and loop on those groups
        for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
            // Set the initial left inset
            var leftInset = sectionInset.left

            // Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
            for attribute in attributes {
                attribute.frame.origin.x = leftInset
                leftInset = attribute.frame.maxX + minimumInteritemSpacing
            }
        }

        return layoutAttributes
    }

}

CollectionViewCell.swift

import UIKit

class CollectionViewCell: UICollectionViewCell {

    override init(frame: CGRect) {
        super.init(frame: frame)

        contentView.backgroundColor = .cyan
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

Spodziewany wynik:

wprowadź opis obrazu tutaj

Imanou Petit
źródło
Czy podczas grupowania atrybutów komórek według wiersza używa 10się dowolnej liczby?
amariduran
27

Proste rozwiązanie w 2019 roku

To jedno z tych przygnębiających pytań, w przypadku których na przestrzeni lat wiele się zmieniło. Teraz jest to łatwe.

Po prostu robisz to:

    // as you move across one row ...
    a.frame.origin.x = x
    x += a.frame.width + minimumInteritemSpacing
    // and, obviously start fresh again each row

Teraz potrzebujesz tylko kodu standardowego:

override func layoutAttributesForElements(
                  in rect: CGRect)->[UICollectionViewLayoutAttributes]? {
    
    guard let att = super.layoutAttributesForElements(in: rect) else { return [] }
    var x: CGFloat = sectionInset.left
    var y: CGFloat = -1.0
    
    for a in att {
        if a.representedElementCategory != .cell { continue }
        
        if a.frame.origin.y >= y { x = sectionInset.left }
        
        a.frame.origin.x = x
        x += a.frame.width + minimumInteritemSpacing
        
        y = a.frame.maxY
    }
    return att
}

Po prostu skopiuj i wklej to do pliku UICollectionViewFlowLayout - gotowe.

W pełni działające rozwiązanie do kopiowania i wklejania:

To jest cała sprawa:

class TagsLayout: UICollectionViewFlowLayout {
    
    required override init() {super.init(); common()}
    required init?(coder aDecoder: NSCoder) {super.init(coder: aDecoder); common()}
    
    private func common() {
        estimatedItemSize = UICollectionViewFlowLayout.automaticSize
        minimumLineSpacing = 10
        minimumInteritemSpacing = 10
    }
    
    override func layoutAttributesForElements(
                    in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        
        guard let att = super.layoutAttributesForElements(in:rect) else {return []}
        var x: CGFloat = sectionInset.left
        var y: CGFloat = -1.0
        
        for a in att {
            if a.representedElementCategory != .cell { continue }
            
            if a.frame.origin.y >= y { x = sectionInset.left }
            a.frame.origin.x = x
            x += a.frame.width + minimumInteritemSpacing
            y = a.frame.maxY
        }
        return att
    }
}

wprowadź opis obrazu tutaj

I w końcu...

Podziękuj @AlexShubin powyżej, który jako pierwszy to wyjaśnił!

Fattie
źródło
Hej. Jestem trochę zmieszany. Dodanie spacji dla każdego ciągu na początku i na końcu nadal pokazuje moje słowo w komórce elementu bez dodanej spacji. Masz jakiś pomysł, jak to zrobić?
Mohamed Lee,
1
Jeśli masz nagłówki sekcji w widoku kolekcji, wydaje się, że je zabijają.
TylerJames
22

Pytanie trwa od jakiegoś czasu, ale nie ma odpowiedzi i jest to dobre pytanie. Odpowiedzią jest przesłonięcie jednej metody w podklasie UICollectionViewFlowLayout:

@implementation MYFlowLayoutSubclass

//Note, the layout's minimumInteritemSpacing (default 10.0) should not be less than this. 
#define ITEM_SPACING 10.0f

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {

    NSArray *attributesForElementsInRect = [super layoutAttributesForElementsInRect:rect];
    NSMutableArray *newAttributesForElementsInRect = [[NSMutableArray alloc] initWithCapacity:attributesForElementsInRect.count];

    CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use.

    //this loop assumes attributes are in IndexPath order
    for (UICollectionViewLayoutAttributes *attributes in attributesForElementsInRect) {
        if (attributes.frame.origin.x == self.sectionInset.left) {
            leftMargin = self.sectionInset.left; //will add outside loop
        } else {
            CGRect newLeftAlignedFrame = attributes.frame;
            newLeftAlignedFrame.origin.x = leftMargin;
            attributes.frame = newLeftAlignedFrame;
        }

        leftMargin += attributes.frame.size.width + ITEM_SPACING;
        [newAttributesForElementsInRect addObject:attributes];
    }   

    return newAttributesForElementsInRect;
}

@end

Zgodnie z zaleceniami Apple, uzyskujesz atrybuty układu z super i iterujesz po nich. Jeśli jest to pierwsza pozycja w wierszu (zdefiniowana przez origin.x znajdujący się na lewym marginesie), zostawiasz go w spokoju i resetujesz x do zera. Następnie dla pierwszej komórki i każdej komórki dodajesz szerokość tej komórki plus margines. Jest to przekazywane do następnego elementu w pętli. Jeśli nie jest to pierwszy element, ustawiasz jego origin.x na bieżący obliczony margines i dodajesz nowe elementy do tablicy.

Mike Sand
źródło
Myślę, że jest to o wiele lepsze niż rozwiązanie Chrisa Wagnera ( stackoverflow.com/a/15554667/482557 ) w kwestii połączonego pytania - Twoja implementacja ma zero powtarzających się wywołań UICollectionViewLayout.
Evan R
1
Nie wygląda to tak, że działa, jeśli wiersz zawiera jeden element, ponieważ UICollectionViewFlowLayout go centruje. Możesz sprawdzić centrum, aby rozwiązać problem. Coś jakattribute.center.x == self.collectionView?.bounds.midX
Ryan Poolos
Zaimplementowałem to, ale z jakiegoś powodu łamie się dla pierwszej sekcji: Pierwsza komórka pojawia się po lewej stronie pierwszego wiersza, a druga komórka (która nie mieści się w tym samym wierszu) przechodzi do następnego wiersza, ale nie po lewej wyrównane. Zamiast tego jego pozycja x zaczyna się tam, gdzie kończy się pierwsza komórka.
Nicolas Miari
@NicolasMiari Pętla decyduje, że jest to nowy wiersz i resetuje za leftMarginpomocą if (attributes.frame.origin.x == self.sectionInset.left) {czeku. Zakłada się, że domyślny układ wyrównał komórkę nowego wiersza do sectionInset.left. Jeśli tak nie jest, wynikałoby z opisanego przez Ciebie zachowania. Wstawiłbym punkt przerwania wewnątrz pętli i sprawdziłbym obie strony pod kątem komórki drugiego rzędu tuż przed porównaniem. Powodzenia.
Mike Sand
3
Dzięki ... Skończyło się na UICollectionViewLeftAlignedLayout: github.com/mokagio/UICollectionViewLeftAlignedLayout . Zastępuje kilka innych metod.
Nicolas Miari
9

Miałem ten sam problem, wypróbuj Cocoapod UICollectionViewLeftAlignedLayout . Po prostu uwzględnij go w swoim projekcie i zainicjuj w następujący sposób:

UICollectionViewLeftAlignedLayout *layout = [[UICollectionViewLeftAlignedLayout alloc] init];
UICollectionView *leftAlignedCollectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout];
mklb
źródło
Brak otwierającego nawiasu kwadratowego w inicjalizacji układu w tym miejscu
RyanOfCourse
Przetestowałem dwie odpowiedzi github.com/eroth/ERJustifiedFlowLayout i UICollectionViewLeftAlignedLayout. Tylko drugi z nich dał prawidłowy wynik przy użyciu assessmentItemSize (self-size).
EckhardN
6

Oto oryginalna odpowiedź w języku Swift. W większości nadal działa świetnie.

class LeftAlignedFlowLayout: UICollectionViewFlowLayout {

    private override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElementsInRect(rect)

        var leftMargin = sectionInset.left

        attributes?.forEach { layoutAttribute in
            if layoutAttribute.frame.origin.x == sectionInset.left {
                leftMargin = sectionInset.left
            }
            else {
                layoutAttribute.frame.origin.x = leftMargin
            }

            leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
        }

        return attributes
    }
}

Wyjątek: automatyczne określanie rozmiaru komórek

Niestety jest jeden duży wyjątek. Jeśli używasz UICollectionViewFlowLayout„s estimatedItemSize. Wewnętrznie UICollectionViewFlowLayoutto trochę zmienia. Nie wyśledziłem tego do końca, ale jasne jest, że wywołuje inne metody po tym layoutAttributesForElementsInRect, jak sam dopasowuję rozmiar komórek. Na podstawie moich prób i błędów stwierdziłem, że layoutAttributesForItemAtIndexPathpodczas autodopasowania częściej dzwoni do każdej komórki indywidualnie. Ta aktualizacja LeftAlignedFlowLayoutdziała świetnie z estimatedItemSize. Działa również z komórkami o statycznym rozmiarze, jednak dodatkowe wywołania układu prowadzą mnie do użycia oryginalnej odpowiedzi zawsze, gdy nie potrzebuję automatycznego określania rozmiaru komórek.

class LeftAlignedFlowLayout: UICollectionViewFlowLayout {

    private override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        let layoutAttribute = super.layoutAttributesForItemAtIndexPath(indexPath)?.copy() as? UICollectionViewLayoutAttributes

        // First in a row.
        if layoutAttribute?.frame.origin.x == sectionInset.left {
            return layoutAttribute
        }

        // We need to align it to the previous item.
        let previousIndexPath = NSIndexPath(forItem: indexPath.item - 1, inSection: indexPath.section)
        guard let previousLayoutAttribute = self.layoutAttributesForItemAtIndexPath(previousIndexPath) else {
            return layoutAttribute
        }

        layoutAttribute?.frame.origin.x = previousLayoutAttribute.frame.maxX + self.minimumInteritemSpacing

        return layoutAttribute
    }
}
Ryan Poolos
źródło
Działa to dla pierwszej sekcji, ale etykieta nie zmienia rozmiaru w drugiej sekcji, dopóki tabela nie zostanie przewinięta. Mój widok kolekcji znajduje się w komórce tabeli.
EI Captain v2.0
5

Opierając się na odpowiedzi Michaela Sanda , stworzyłem podklasęUICollectionViewFlowLayout bibliotekę z aby wykonać do lewej, prawej lub pełnej (w zasadzie domyślnej) poziomej - pozwala ona również ustawić bezwzględną odległość między każdą komórką. Planuję dodać do tego wyrównanie do środka w poziomie i wyrównanie w pionie.

https://github.com/eroth/ERJustifiedFlowLayout

Evan R
źródło
5

Szybko. Według odpowiedzi Michaelsa

override func layoutAttributesForElementsInRect(rect: CGRect) ->     [UICollectionViewLayoutAttributes]? {
    guard let oldAttributes = super.layoutAttributesForElementsInRect(rect) else {
        return super.layoutAttributesForElementsInRect(rect)
    }
    let spacing = CGFloat(50) // REPLACE WITH WHAT SPACING YOU NEED
    var newAttributes = [UICollectionViewLayoutAttributes]()
    var leftMargin = self.sectionInset.left
    for attributes in oldAttributes {
        if (attributes.frame.origin.x == self.sectionInset.left) {
            leftMargin = self.sectionInset.left
        } else {
            var newLeftAlignedFrame = attributes.frame
            newLeftAlignedFrame.origin.x = leftMargin
            attributes.frame = newLeftAlignedFrame
        }

        leftMargin += attributes.frame.width + spacing
        newAttributes.append(attributes)
    }
    return newAttributes
}
GregP
źródło
4

Na podstawie wszystkich odpowiedzi trochę się zmieniam i to działa dobrze

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    let attributes = super.layoutAttributesForElements(in: rect)

    var leftMargin = sectionInset.left
    var maxY: CGFloat = -1.0


    attributes?.forEach { layoutAttribute in
        if layoutAttribute.frame.origin.y >= maxY
                   || layoutAttribute.frame.origin.x == sectionInset.left {
            leftMargin = sectionInset.left
        }

        if layoutAttribute.frame.origin.x == sectionInset.left {
            leftMargin = sectionInset.left
        }
        else {
            layoutAttribute.frame.origin.x = leftMargin
        }

        leftMargin += layoutAttribute.frame.width
        maxY = max(layoutAttribute.frame.maxY, maxY)
    }

    return attributes
}
kotvaska
źródło
4

Jeśli ktoś z was ma problem - niektóre komórki po prawej stronie widoku kolekcji przekraczają granice widoku kolekcji . Następnie użyj tego -

class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElements(in: rect)

        var leftMargin : CGFloat = sectionInset.left
        var maxY: CGFloat = -1.0
        attributes?.forEach { layoutAttribute in
            if Int(layoutAttribute.frame.origin.y) >= Int(maxY) {
                leftMargin = sectionInset.left
            }

            layoutAttribute.frame.origin.x = leftMargin

            leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
            maxY = max(layoutAttribute.frame.maxY , maxY)
        }
        return attributes
    }
}

Użyj INT zamiast porównywania wartości CGFloat .

niku
źródło
3

Na podstawie odpowiedzi tutaj, ale naprawiono awarie i problemy z wyrównywaniem, gdy widok kolekcji zawiera również nagłówki lub stopki. Wyrównywanie tylko do lewej komórki:

class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElements(in: rect)

        var leftMargin = sectionInset.left
        var prevMaxY: CGFloat = -1.0
        attributes?.forEach { layoutAttribute in

            guard layoutAttribute.representedElementCategory == .cell else {
                return
            }

            if layoutAttribute.frame.origin.y >= prevMaxY {
                leftMargin = sectionInset.left
            }

            layoutAttribute.frame.origin.x = leftMargin

            leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
            prevMaxY = layoutAttribute.frame.maxY
        }

        return attributes
    }
}
Alex Shubin
źródło
2

Dzięki za odpowiedź Michaela Sanda . Zmodyfikowałem go do rozwiązania wielu wierszy (to samo wyrównanie do góry y każdego wiersza), czyli wyrównanie do lewej, równe odstępy do każdego elementu.

static CGFloat const ITEM_SPACING = 10.0f;

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    CGRect contentRect = {CGPointZero, self.collectionViewContentSize};

    NSArray *attributesForElementsInRect = [super layoutAttributesForElementsInRect:contentRect];
    NSMutableArray *newAttributesForElementsInRect = [[NSMutableArray alloc] initWithCapacity:attributesForElementsInRect.count];

    CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use.
    NSMutableDictionary *leftMarginDictionary = [[NSMutableDictionary alloc] init];

    for (UICollectionViewLayoutAttributes *attributes in attributesForElementsInRect) {
        UICollectionViewLayoutAttributes *attr = attributes.copy;

        CGFloat lastLeftMargin = [[leftMarginDictionary valueForKey:[[NSNumber numberWithFloat:attributes.frame.origin.y] stringValue]] floatValue];
        if (lastLeftMargin == 0) lastLeftMargin = leftMargin;

        CGRect newLeftAlignedFrame = attr.frame;
        newLeftAlignedFrame.origin.x = lastLeftMargin;
        attr.frame = newLeftAlignedFrame;

        lastLeftMargin += attr.frame.size.width + ITEM_SPACING;
        [leftMarginDictionary setObject:@(lastLeftMargin) forKey:[[NSNumber numberWithFloat:attributes.frame.origin.y] stringValue]];
        [newAttributesForElementsInRect addObject:attr];
    }

    return newAttributesForElementsInRect;
}
Shu Zhang
źródło
1

Oto moja podróż w celu znalezienia najlepszego kodu, który działa ze Swift 5. Połączyłem kilka odpowiedzi z tego wątku i kilku innych wątków, aby rozwiązać ostrzeżenia i problemy, z którymi się spotkałem. Podczas przewijania widoku kolekcji wystąpiło ostrzeżenie i pewne nietypowe zachowanie. Konsola wyświetla następujące informacje:

Dzieje się tak prawdopodobnie, ponieważ układ przepływu „xyz” modyfikuje atrybuty zwracane przez UICollectionViewFlowLayout bez ich kopiowania.

Inną kwestią, z którą się spotkałem, jest to, że niektóre długie komórki są przycinane po prawej stronie ekranu. Ponadto ustawiłem wstawki sekcji i minimumInteritemSpacing w funkcjach delegatów, co spowodowało, że wartości nie zostały odzwierciedlone w klasie niestandardowej. Rozwiązaniem było ustawienie tych atrybutów w instancji układu przed zastosowaniem go w moim widoku kolekcji.

Oto jak użyłem układu widoku mojej kolekcji:

let layout = LeftAlignedCollectionViewFlowLayout()
layout.minimumInteritemSpacing = 5
layout.minimumLineSpacing = 7.5
layout.sectionInset = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
super.init(frame: frame, collectionViewLayout: layout)

Oto klasa układu przepływu

class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElements(in: rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }

        var leftMargin = sectionInset.left
        var maxY: CGFloat = -1.0
        attributes?.forEach { layoutAttribute in
            guard layoutAttribute.representedElementCategory == .cell else {
                return
            }

            if Int(layoutAttribute.frame.origin.y) >= Int(maxY) || layoutAttribute.frame.origin.x == sectionInset.left {
                leftMargin = sectionInset.left
            }

            if layoutAttribute.frame.origin.x == sectionInset.left {
                leftMargin = sectionInset.left
            }
            else {
                layoutAttribute.frame.origin.x = leftMargin
            }

            leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
            maxY = max(layoutAttribute.frame.maxY , maxY)
        }

        return attributes
    }
}
Youstanzr
źródło
Uratowałeś mi dzień
David 'mArm' Ansermot
0

Problem z UICollectionView polega na tym, że próbuje automatycznie dopasować komórki w dostępnym obszarze. Zrobiłem to, najpierw definiując liczbę wierszy i kolumn, a następnie definiując rozmiar komórki dla tego wiersza i kolumny

1) Aby zdefiniować sekcje (wiersze) mojego UICollectionView:

(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView

2) Określenie liczby pozycji w sekcji. Możesz zdefiniować inną liczbę pozycji dla każdej sekcji. numer sekcji można uzyskać za pomocą parametru „sekcja”.

(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section

3) Aby zdefiniować rozmiar komórki osobno dla każdej sekcji i wiersza. Numer sekcji i numer wiersza można uzyskać za pomocą parametru „indexPath”, tj. [indexPath section]Dla numeru sekcji i [indexPath row]dla numeru wiersza.

(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath

4) Następnie możesz wyświetlić komórki w wierszach i sekcjach za pomocą:

(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath

UWAGA: w UICollectionView

Section == Row
IndexPath.Row == Column
Anas iqbal
źródło
0

Odpowiedź Mike'a Sanda jest dobra, ale miałem pewne problemy z tym kodem (jak np. Wycięcie długiej komórki). I nowy kod:

#define ITEM_SPACE 7.0f

@implementation LeftAlignedCollectionViewFlowLayout
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSArray* attributesToReturn = [super layoutAttributesForElementsInRect:rect];
    for (UICollectionViewLayoutAttributes* attributes in attributesToReturn) {
        if (nil == attributes.representedElementKind) {
            NSIndexPath* indexPath = attributes.indexPath;
            attributes.frame = [self layoutAttributesForItemAtIndexPath:indexPath].frame;
        }
    }
    return attributesToReturn;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewLayoutAttributes* currentItemAttributes =
    [super layoutAttributesForItemAtIndexPath:indexPath];

    UIEdgeInsets sectionInset = [(UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout sectionInset];

    if (indexPath.item == 0) { // first item of section
        CGRect frame = currentItemAttributes.frame;
        frame.origin.x = sectionInset.left; // first item of the section should always be left aligned
        currentItemAttributes.frame = frame;

        return currentItemAttributes;
    }

    NSIndexPath* previousIndexPath = [NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section];
    CGRect previousFrame = [self layoutAttributesForItemAtIndexPath:previousIndexPath].frame;
    CGFloat previousFrameRightPoint = previousFrame.origin.x + previousFrame.size.width + ITEM_SPACE;

    CGRect currentFrame = currentItemAttributes.frame;
    CGRect strecthedCurrentFrame = CGRectMake(0,
                                              currentFrame.origin.y,
                                              self.collectionView.frame.size.width,
                                              currentFrame.size.height);

    if (!CGRectIntersectsRect(previousFrame, strecthedCurrentFrame)) { // if current item is the first item on the line
        // the approach here is to take the current frame, left align it to the edge of the view
        // then stretch it the width of the collection view, if it intersects with the previous frame then that means it
        // is on the same line, otherwise it is on it's own new line
        CGRect frame = currentItemAttributes.frame;
        frame.origin.x = sectionInset.left; // first item on the line should always be left aligned
        currentItemAttributes.frame = frame;
        return currentItemAttributes;
    }

    CGRect frame = currentItemAttributes.frame;
    frame.origin.x = previousFrameRightPoint;
    currentItemAttributes.frame = frame;
    return currentItemAttributes;
}
Lal Krishna
źródło
0

Zredagowana odpowiedź Angel García Olloqui na szacunek minimumInteritemSpacingdelegata collectionView(_:layout:minimumInteritemSpacingForSectionAt:), jeśli ją wdraża.

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    let attributes = super.layoutAttributesForElements(in: rect)

    var leftMargin = sectionInset.left
    var maxY: CGFloat = -1.0
    attributes?.forEach { layoutAttribute in
        if layoutAttribute.frame.origin.y >= maxY {
            leftMargin = sectionInset.left
        }

        layoutAttribute.frame.origin.x = leftMargin

        let delegate = collectionView?.delegate as? UICollectionViewDelegateFlowLayout
        let spacing = delegate?.collectionView?(collectionView!, layout: self, minimumInteritemSpacingForSectionAt: 0) ?? minimumInteritemSpacing

        leftMargin += layoutAttribute.frame.width + spacing
        maxY = max(layoutAttribute.frame.maxY , maxY)
    }

    return attributes
}
Radek
źródło
0

Powyższy kod działa u mnie. Chciałbym udostępnić odpowiedni kod Swift 3.0.

class SFFlowLayout: UICollectionViewFlowLayout {

    let itemSpacing: CGFloat = 3.0

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

        let attriuteElementsInRect = super.layoutAttributesForElements(in: rect)
        var newAttributeForElement: Array<UICollectionViewLayoutAttributes> = []
        var leftMargin = self.sectionInset.left
        for tempAttribute in attriuteElementsInRect! {
            let attribute = tempAttribute 
            if attribute.frame.origin.x == self.sectionInset.left {
                leftMargin = self.sectionInset.left
            }
            else {
                var newLeftAlignedFrame = attribute.frame
                newLeftAlignedFrame.origin.x = leftMargin
                attribute.frame = newLeftAlignedFrame
            }
            leftMargin += attribute.frame.size.width + itemSpacing
            newAttributeForElement.append(attribute)
        }
        return newAttributeForElement
    }
}
Naresh
źródło