Links ausgerichtete Zellen in UICollectionView

95

Ich verwende in meinem Projekt eine UICollectionView, in der mehrere Zellen unterschiedlicher Breite in einer Linie vorhanden sind. Laut: https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html

Es verteilt die Zellen mit gleicher Polsterung über die Linie. Dies geschieht wie erwartet, außer ich möchte sie linksbündig ausrichten und eine Auffüllbreite fest codieren.

Ich glaube, ich muss UICollectionViewFlowLayout in eine Unterklasse unterteilen, aber nachdem ich einige der Tutorials usw. online gelesen habe, verstehe ich einfach nicht, wie das funktioniert.

Damien
quelle

Antworten:

166

Die anderen Lösungen in diesem Thread funktionieren nicht richtig, wenn die Zeile nur aus 1 Element besteht oder zu kompliziert ist.

Anhand des Beispiels von Ryan habe ich den Code geändert, um eine neue Zeile zu erkennen, indem ich die Y-Position des neuen Elements überprüft habe. Sehr einfach und schnell in der Leistung.

Schnell:

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

Wenn Sie möchten, dass zusätzliche Ansichten ihre Größe beibehalten, fügen Sie oben im Abschluss des forEachAufrufs Folgendes hinzu :

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

Ziel 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;
}
Engel G. Olloqui
quelle
4
Vielen Dank! Ich erhielt eine Warnung, die behoben wird, indem eine Kopie der Attribute im Array erstellt wird let attributes = super.layoutAttributesForElementsInRect(rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }
tkuichooseyou
2
Für alle, die wie ich auf die Suche nach einer zentrierten
Alex Koshy
Ich habe diese Lösung ausprobiert, aber ein Teil der Zelle rechts in der Sammlungsansicht überschreitet die Grenzen der Sammlungsansicht. Ist noch jemand darauf gestoßen?
Happiehappie
@Happiehappie Ja, ich habe dieses Verhalten auch. Irgendein Fix?
John Baum
Versuchte diese Lösung, konnte das Layout jedoch nicht mit der collectionView im Bedienfeld „Interface Builder-Dienstprogramme“ verknüpfen. Am Ende tat es durch Code. Irgendein Hinweis warum?
Acrespo
60

In den Antworten auf diese Frage sind viele großartige Ideen enthalten. Die meisten von ihnen haben jedoch einige Nachteile:

  • Lösungen, die den y- Wert der Zelle nicht überprüfen, funktionieren nur für einzeilige Layouts . Sie schlagen für Layouts von Sammlungsansichten mit mehreren Zeilen fehl.
  • Lösungen, die den y- Wert überprüfen , wie die Antwort von Angel García Olloqui, funktionieren nur, wenn alle Zellen dieselbe Höhe haben . Sie schlagen für Zellen mit variabler Höhe fehl.
  • Die meisten Lösungen überschreiben nur die layoutAttributesForElements(in rect: CGRect)Funktion. Sie überschreiben nicht layoutAttributesForItem(at indexPath: IndexPath). Dies ist ein Problem, da die Sammlungsansicht die letztere Funktion regelmäßig aufruft, um die Layoutattribute für einen bestimmten Indexpfad abzurufen. Wenn Sie von dieser Funktion nicht die richtigen Attribute zurückgeben, treten wahrscheinlich alle Arten von visuellen Fehlern auf, z. B. beim Einfügen und Löschen von Animationen von Zellen oder bei Verwendung von Zellen mit Selbstgröße durch Festlegen des Layouts der Sammlungsansicht estimatedItemSize. In den Apple-Dokumenten heißt es :

    Von jedem benutzerdefinierten Layoutobjekt wird erwartet, dass es die layoutAttributesForItemAtIndexPath:Methode implementiert .

  • Viele Lösungen machen auch Annahmen über den rectParameter, der an die layoutAttributesForElements(in rect: CGRect)Funktion übergeben wird. Zum Beispiel basieren viele auf der Annahme, dass das rectimmer am Anfang einer neuen Zeile beginnt, was nicht unbedingt der Fall ist.

Also mit anderen Worten:

Die meisten der auf dieser Seite vorgeschlagenen Lösungen funktionieren für bestimmte Anwendungen, funktionieren jedoch nicht in jeder Situation wie erwartet.


AlignedCollectionViewFlowLayout

Um diese Probleme anzugehen, habe ich eine UICollectionViewFlowLayoutUnterklasse erstellt, die einer ähnlichen Idee folgt, wie sie von Matt und Chris Wagner in ihren Antworten auf eine ähnliche Frage vorgeschlagen wurde. Es kann entweder die Zellen ausrichten

⬅︎ links :

Linksbündiges Layout

oder ➡︎ richtig :

Rechts ausgerichtetes Layout

und bietet zusätzlich Optionen zum vertikalen Ausrichten der Zellen in ihren jeweiligen Zeilen (falls sie in der Höhe variieren).

Sie können es einfach hier herunterladen:

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

Die Verwendung ist unkompliziert und wird in der README-Datei erläutert. Sie erstellen im Grunde eine Instanz von AlignedCollectionViewFlowLayout, geben die gewünschte Ausrichtung an und weisen sie der collectionViewLayoutEigenschaft Ihrer Sammlungsansicht zu :

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

 yourCollectionView.collectionViewLayout = alignedFlowLayout

(Es ist auch auf Cocoapods erhältlich .)


So funktioniert es (für linksbündige Zellen):

Das Konzept hier besteht darin, sich ausschließlich auf die layoutAttributesForItem(at indexPath: IndexPath)Funktion zu verlassen . In erhalten layoutAttributesForElements(in rect: CGRect)wir einfach die Indexpfade aller Zellen innerhalb der rectund rufen dann die erste Funktion für jeden Indexpfad auf, um die richtigen Frames abzurufen:

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
}

(Die copy()Funktion erstellt einfach eine tiefe Kopie aller Layoutattribute im Array. Sie können den Quellcode für seine Implementierung untersuchen.)

Jetzt müssen wir nur noch die layoutAttributesForItem(at indexPath: IndexPath)Funktion richtig implementieren . Die Superklasse fügt UICollectionViewFlowLayoutbereits die richtige Anzahl von Zellen in jede Zeile ein, sodass wir sie nur innerhalb ihrer jeweiligen Zeile nach links verschieben müssen. Die Schwierigkeit liegt in der Berechnung des Platzbedarfs, den wir benötigen, um jede Zelle nach links zu verschieben.

Da wir einen festen Abstand zwischen den Zellen haben möchten, besteht die Kernidee darin, einfach anzunehmen, dass die vorherige Zelle (die Zelle links von der aktuell angelegten Zelle) bereits richtig positioniert ist. Dann müssen wir nur den Zellenabstand zum maxXWert des Rahmens der vorherigen Zelle hinzufügen , und das ist der origin.xWert für den Rahmen der aktuellen Zelle.

Jetzt müssen wir nur noch wissen, wann wir den Anfang einer Zeile erreicht haben, damit wir keine Zelle neben einer Zelle in der vorherigen Zeile ausrichten. (Dies würde nicht nur zu einem falschen Layout führen, sondern auch extrem verzögert.) Wir benötigen also einen Rekursionsanker. Der Ansatz, mit dem ich diesen Rekursionsanker finde, ist der folgende:

Um herauszufinden, ob sich die Zelle am Index i in derselben Zeile befindet wie die Zelle mit dem Index i-1 ...

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

... Ich "zeichne" ein Rechteck um die aktuelle Zelle und strecke es über die Breite der gesamten Sammlungsansicht. Da die UICollectionViewFlowLayoutZentren alle Zellen vertikal zentrieren, muss sich jede Zelle in derselben Linie mit diesem Rechteck schneiden.

Daher überprüfe ich einfach, ob sich die Zelle mit dem Index i-1 mit diesem Linienrechteck schneidet, das aus der Zelle mit dem Index i erstellt wurde .

  • Wenn es sich schneidet, ist die Zelle mit dem Index i nicht die Zelle ganz links in der Zeile.
    → Holen Sie sich den Frame der vorherigen Zelle (mit dem Index i - 1 ) und verschieben Sie die aktuelle Zelle daneben.

  • Wenn es sich nicht schneidet, ist die Zelle mit dem Index i die Zelle ganz links in der Zeile.
    → Bewegen Sie die Zelle an den linken Rand der Sammlungsansicht (ohne ihre vertikale Position zu ändern).

Ich werde die tatsächliche Implementierung der layoutAttributesForItem(at indexPath: IndexPath)Funktion hier nicht veröffentlichen, da ich denke, dass der wichtigste Teil darin besteht, die Idee zu verstehen , und Sie meine Implementierung immer im Quellcode überprüfen können . (Es ist etwas komplizierter als hier erklärt, da ich auch die .rightAusrichtung und verschiedene vertikale Ausrichtungsoptionen zulasse . Es folgt jedoch der gleichen Idee.)


Wow, ich denke, dies ist die längste Antwort, die ich jemals auf Stackoverflow geschrieben habe. Ich hoffe das hilft. 😉

Mischa
quelle
Ich bewerbe mich mit diesem Szenario stackoverflow.com/questions/56349052/…, aber es funktioniert nicht. Würden Sie mir sagen, wie ich das erreichen kann
Nazmul Hasan
Selbst mit diesem (was ein wirklich großartiges Projekt ist) erlebe ich ein Flashen in der vertikalen Bildlaufsammlungsansicht meiner Apps. Ich habe 20 Zellen, und beim ersten Bildlauf ist die erste Zelle leer und alle Zellen sind um eine Position nach rechts verschoben. Nach einem vollständigen Bildlauf nach oben und unten werden die Dinge richtig ausgerichtet.
Parth Tamane
Tolles Projekt! Es wäre großartig zu sehen, dass es für Karthago aktiviert ist.
pjuzeliunas
30

Mit Swift 4.1 und iOS 11 können Sie je nach Bedarf eine der beiden folgenden vollständigen Implementierungen auswählen , um Ihr Problem zu beheben.


# 1. Autoresizing links ausrichten UICollectionViewCells

Die folgende Implementierung zeigt, wie UICollectionViewLayout's layoutAttributesForElements(in:), UICollectionViewFlowLayout' s estimatedItemSizeund UILabel's verwendet preferredMaxLayoutWidthwerden, um Autoresizing-Zellen in a UICollectionView: links auszurichten.

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

}

Erwartetes Ergebnis:

Geben Sie hier die Bildbeschreibung ein


# 2. Links UICollectionViewCellmit fester Größe ausrichten

Die folgende Implementierung zeigt, wie Sie UICollectionViewLayout's layoutAttributesForElements(in:)und UICollectionViewFlowLayout' s itemSizeverwenden, um Zellen mit vordefinierter Größe in a links auszurichten UICollectionView:

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

}

Erwartetes Ergebnis:

Geben Sie hier die Bildbeschreibung ein

Imanou Petit
quelle
Wird beim Gruppieren von Zellenattributen nach Zeilen 10eine beliebige Zahl verwendet?
Amariduran
24

Die einfache Lösung im Jahr 2019

Dies ist eine dieser deprimierenden Fragen, bei denen sich die Dinge im Laufe der Jahre stark verändert haben. Es ist jetzt einfach.

Grundsätzlich machst du einfach Folgendes:

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

Jetzt brauchen Sie nur noch den Boilerplate-Code:

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
}

Kopieren Sie das einfach und fügen Sie es in a ein UICollectionViewFlowLayout- fertig.

Voll funktionsfähige Lösung zum Kopieren und Einfügen:

Das ist das Ganze:

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

Geben Sie hier die Bildbeschreibung ein

Und schlussendlich...

Bedanken Sie sich bei @AlexShubin oben, der dies zuerst geklärt hat!

Fattie
quelle
Hallo. Ich bin etwas durcheinander. Wenn Sie am Anfang und am Ende für jede Zeichenfolge Leerzeichen hinzufügen, wird mein Wort in der Elementzelle ohne das von mir hinzugefügte Leerzeichen angezeigt. Irgendeine Idee, wie es geht?
Mohamed Lee
Wenn Ihre Sammlungsansicht Abschnittsüberschriften enthält, scheint dies diese zu töten.
TylerJames
22

Die Frage ist schon eine Weile offen, aber es gibt keine Antwort und es ist eine gute Frage. Die Antwort besteht darin, eine Methode in der UICollectionViewFlowLayout-Unterklasse zu überschreiben:

@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

Wie von Apple empfohlen, erhalten Sie die Layoutattribute von super und durchlaufen sie. Wenn es das erste in der Zeile ist (definiert durch seinen Ursprung.x am linken Rand), lassen Sie es in Ruhe und setzen das x auf Null zurück. Dann addieren Sie für die erste Zelle und jede Zelle die Breite dieser Zelle plus einen Rand. Dies wird an das nächste Element in der Schleife übergeben. Wenn es nicht das erste Element ist, setzen Sie origin.x auf den laufenden berechneten Rand und fügen dem Array neue Elemente hinzu.

Mike Sand
quelle
Ich denke, dies ist viel besser als Chris Wagners Lösung ( stackoverflow.com/a/15554667/482557 ) für die verknüpfte Frage - Ihre Implementierung hat keine sich wiederholenden UICollectionViewLayout-Aufrufe.
Evan R
1
Sieht nicht so aus, wenn die Zeile ein einzelnes Element enthält, da UICollectionViewFlowLayout es zentriert. Sie können das Zentrum überprüfen, um das Problem zu lösen. So etwas wieattribute.center.x == self.collectionView?.bounds.midX
Ryan Poolos
Ich habe dies implementiert, aber aus irgendeinem Grund wird es für den ersten Abschnitt unterbrochen: Die erste Zelle wird links in der ersten Zeile angezeigt, und die zweite Zelle (die nicht in dieselbe Zeile passt) wird in die nächste Zeile verschoben, jedoch nicht links ausgerichtet. Stattdessen beginnt seine x-Position dort, wo die erste Zelle endet.
Nicolas Miari
@NicolasMiari Die Schleife entscheidet, dass es sich um eine neue Zeile handelt, und setzt die leftMarginmit der if (attributes.frame.origin.x == self.sectionInset.left) {Prüfung zurück. Dies setzt voraus, dass das Standardlayout die Zelle der neuen Zeile so ausgerichtet hat, dass sie mit der Zelle bündig ist sectionInset.left. Ist dies nicht der Fall, würde sich das von Ihnen beschriebene Verhalten ergeben. Ich würde einen Haltepunkt in die Schleife einfügen und beide Seiten kurz vor dem Vergleich auf die Zelle der zweiten Zeile prüfen. Viel Glück.
Mike Sand
3
Vielen Dank ... Ich habe schließlich UICollectionViewLeftAlignedLayout verwendet: github.com/mokagio/UICollectionViewLeftAlignedLayout . Es überschreibt ein paar weitere Methoden.
Nicolas Miari
9

Ich hatte das gleiche Problem: Probieren Sie das Cocoapod UICollectionViewLeftAlignedLayout aus . Fügen Sie es einfach in Ihr Projekt ein und initialisieren Sie es wie folgt:

UICollectionViewLeftAlignedLayout *layout = [[UICollectionViewLeftAlignedLayout alloc] init];
UICollectionView *leftAlignedCollectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout];
mklb
quelle
Fehlende eckige Klammer in Ihrer Initialisierung des Layouts dort
RyanOfCourse
Ich habe die beiden Antworten github.com/eroth/ERJustifiedFlowLayout und UICollectionViewLeftAlignedLayout getestet. Nur der zweite hat das richtige Ergebnis erzielt, wenn EstimatedItemSize (Self-Sizing) verwendet wurde.
EckhardN
5

Aufbauend auf der Antwort von Michael Sand habe ich eine UICollectionViewFlowLayoutBibliothek mit Unterklassen erstellt , die links, rechts oder vollständig (im Grunde die Standardeinstellung) horizontal ausgerichtet ist. Außerdem können Sie den absoluten Abstand zwischen den einzelnen Zellen festlegen. Ich habe vor, auch eine horizontale und eine vertikale Ausrichtung hinzuzufügen.

https://github.com/eroth/ERJustifiedFlowLayout

Evan R.
quelle
5

Hier ist die ursprüngliche Antwort in Swift. Meistens funktioniert es immer noch großartig.

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

Ausnahme: Autosizing-Zellen

Es gibt leider eine große Ausnahme. Wenn Sie UICollectionViewFlowLayout's verwenden estimatedItemSize. Intern UICollectionViewFlowLayoutändert sich etwas. Ich habe es nicht vollständig aufgespürt, aber es ist klar, dass es andere Methoden aufruft, layoutAttributesForElementsInRectwährend die Zellen selbst dimensioniert werden. Aus meinem Versuch und Irrtum habe ich herausgefunden, dass es layoutAttributesForItemAtIndexPathbei der automatischen Größenanpassung häufiger darum geht, jede Zelle einzeln aufzurufen. Dieses Update LeftAlignedFlowLayoutfunktioniert hervorragend mit estimatedItemSize. Es funktioniert auch mit Zellen mit statischer Größe. Die zusätzlichen Layoutaufrufe führen jedoch dazu, dass ich die ursprüngliche Antwort immer dann verwende, wenn ich keine Zellen mit automatischer Größe benötige.

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
quelle
Dies funktioniert für den ersten Abschnitt, die Größe des Etiketts wird jedoch erst im zweiten Abschnitt geändert, wenn die Tabelle gescrollt wird. Meine Sammlungsansicht befindet sich in der Tabellenzelle.
EI Captain v2.0
5

In kurzer Zeit. Laut Michaels Antwort

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
quelle
4

Aufgrund aller Antworten ändere ich mich ein wenig und es funktioniert gut für mich

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
quelle
4

Wenn jemand von Ihnen Probleme hat - einige der Zellen rechts in der Sammlungsansicht überschreiten die Grenzen der Sammlungsansicht . Dann benutzen Sie bitte diese -

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

Verwenden Sie INT anstelle des Vergleichs der CGFloat- Werte.

Niku
quelle
3

Basierend auf den Antworten hier, aber behobenen Abstürzen und Ausrichtungsproblemen, wenn Ihre Sammlungsansicht auch Kopf- oder Fußzeilen enthält. Nur linke Zellen ausrichten:

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
quelle
2

Danke für die Antwort von Michael Sand . Ich habe es in eine Lösung aus mehreren Zeilen (dieselbe Ausrichtung oben y jeder Zeile) geändert, bei der es sich um die linke Ausrichtung handelt, sogar um den Abstand zu jedem Element.

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
quelle
1

Hier ist meine Reise, um den besten Code zu finden, der mit Swift 5 funktioniert. Ich habe einige Antworten aus diesem Thread und einigen anderen Threads zusammengeführt, um Warnungen und Probleme zu lösen, mit denen ich konfrontiert war. Ich hatte eine Warnung und ein abnormales Verhalten beim Scrollen durch meine Sammlungsansicht. Die Konsole druckt Folgendes:

Dies tritt wahrscheinlich auf, weil das Flusslayout "xyz" die von UICollectionViewFlowLayout zurückgegebenen Attribute ändert, ohne sie zu kopieren.

Ein weiteres Problem, mit dem ich konfrontiert war, ist, dass einige lange Zellen auf der rechten Seite des Bildschirms abgeschnitten werden. Außerdem habe ich die Abschnittseinfügungen und das MinimumInteritemSpacing in den Delegatfunktionen festgelegt, was dazu führte, dass Werte nicht in der benutzerdefinierten Klasse wiedergegeben wurden. Das Problem wurde behoben, indem diese Attribute auf eine Instanz des Layouts festgelegt wurden, bevor sie auf meine Sammlungsansicht angewendet wurden.

So habe ich das Layout für meine Sammlungsansicht verwendet:

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)

Hier ist die Flow-Layout-Klasse

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
quelle
Du hast meinen Tag gerettet
David 'mArm' Ansermot
0

Das Problem mit UICollectionView besteht darin, dass versucht wird, die Zellen automatisch in den verfügbaren Bereich einzupassen. Ich habe dies getan, indem ich zuerst die Anzahl der Zeilen und Spalten und dann die Zellengröße für diese Zeile und Spalte definiert habe

1) So definieren Sie Abschnitte (Zeilen) meiner UICollectionView:

(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView

2) Definieren der Anzahl der Elemente in einem Abschnitt. Sie können für jeden Abschnitt eine andere Anzahl von Elementen definieren. Sie können die Abschnittsnummer mit dem Parameter 'section' abrufen.

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

3) Definieren der Zellengröße für jeden Abschnitt und jede Zeile separat. Sie können die Abschnittsnummer und die Zeilennummer mit dem Parameter 'indexPath' abrufen, dh [indexPath section]für die Abschnittsnummer und die [indexPath row]Zeilennummer.

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

4) Anschließend können Sie Ihre Zellen in Zeilen und Abschnitten anzeigen, indem Sie:

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

HINWEIS: In UICollectionView

Section == Row
IndexPath.Row == Column
Anas iqbal
quelle
0

Die Antwort von Mike Sand ist gut, aber ich hatte einige Probleme mit diesem Code (wie lange Zellen herausgeschnitten). Und neuer Code:

#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
quelle
0

Die Antwort minimumInteritemSpacingvon Angel García Olloqui auf den Respekt der Delegierten wurde bearbeitet collectionView(_:layout:minimumInteritemSpacingForSectionAt:), wenn sie umgesetzt wird.

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
quelle
0

Der obige Code funktioniert für mich. Ich möchte den jeweiligen Swift 3.0-Code teilen.

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
quelle