Alinear celdas a la izquierda en UICollectionView

96

Estoy usando un UICollectionView en mi proyecto, donde hay varias celdas de diferentes anchos en una línea. Según: https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html

extiende las celdas a lo largo de la línea con el mismo relleno. Esto sucede como se esperaba, excepto que quiero justificarlos a la izquierda y codificar un ancho de relleno.

Supongo que necesito subclasificar UICollectionViewFlowLayout, sin embargo, después de leer algunos de los tutoriales, etc.en línea, parece que no entiendo cómo funciona esto.

Damien
fuente

Respuestas:

169

Las otras soluciones en este hilo no funcionan correctamente, cuando la línea está compuesta por solo 1 artículo o son demasiado complicadas.

Basado en el ejemplo dado por Ryan, cambié el código para detectar una nueva línea inspeccionando la posición Y del nuevo elemento. Muy simple y de ejecución rápida.

Rápido:

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

Si desea que las vistas complementarias mantengan su tamaño, agregue lo siguiente en la parte superior del cierre de la forEachconvocatoria:

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

C objetivo:

- (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;
}
Ángel G. Olloqui
fuente
4
¡Gracias! Recibí una advertencia, que se soluciona al hacer una copia de los atributos en la matriz let attributes = super.layoutAttributesForElementsInRect(rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }
tkuichooseyou
2
Para cualquiera que haya tropezado con esto buscando una versión alineada al centro como lo hice yo, publiqué una versión modificada de la respuesta de Angel aquí: stackoverflow.com/a/38254368/2845237
Alex Koshy
Probé esta solución, pero parte de la celda que está a la derecha de la vista de colección excede los límites de la vista de colección. ¿Alguien más se encontró con esto?
Happiehappie
@Happiehappie sí, yo también tengo ese comportamiento. alguna solución?
John Baum
Probé esta solución pero no pude asociar el diseño con collectionView en el panel de utilidades de Interface Builder. Terminé haciéndolo por código. ¿Alguna pista por qué?
acrespo
63

Hay muchas grandes ideas incluidas en las respuestas a esta pregunta. Sin embargo, la mayoría tiene algunos inconvenientes:

  • Las soluciones que no comprueban el valor y de la celda solo funcionan para diseños de una sola línea . Fallan para diseños de vista de colección con varias líneas.
  • Las soluciones que hacen comprobar el y valor, como la respuesta de Angel García Olloqui sólo funcionan si todas las células tienen la misma altura . Fallan para celdas con altura variable.
  • La mayoría de las soluciones solo anulan la layoutAttributesForElements(in rect: CGRect)función. No anulan layoutAttributesForItem(at indexPath: IndexPath). Esto es un problema porque la vista de colección llama periódicamente a la última función para recuperar los atributos de diseño para una ruta de índice en particular. Si no devuelve los atributos adecuados de esa función, es probable que se encuentre con todo tipo de errores visuales, por ejemplo, durante la inserción y eliminación de animaciones de celdas o al usar celdas de tamaño propio configurando el diseño de la vista de colección estimatedItemSize. Los documentos de Apple dicen:

    Se espera que cada objeto de diseño personalizado implemente el layoutAttributesForItemAtIndexPath:método.

  • Muchas soluciones también hacen suposiciones sobre el rectparámetro que se pasa a la layoutAttributesForElements(in rect: CGRect)función. Por ejemplo, muchos se basan en la suposición de que rectsiempre comienza al principio de una nueva línea, lo que no es necesariamente el caso.

Entonces en otras palabras:

La mayoría de las soluciones sugeridas en esta página funcionan para algunas aplicaciones específicas, pero no funcionan como se esperaba en todas las situaciones.


AlignedCollectionViewFlowLayout

Para abordar estos problemas, he creado una UICollectionViewFlowLayoutsubclase que sigue una idea similar a la sugerida por Matt y Chris Wagner en sus respuestas a una pregunta similar. Puede alinear las celdas

⬅︎ izquierda :

Diseño alineado a la izquierda

o ➡︎ bien :

Diseño alineado a la derecha

y además ofrece opciones para verticalmente alinear las celdas en sus respectivas filas (en caso de que varíen en altura).

Simplemente puede descargarlo aquí:

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

El uso es sencillo y se explica en el archivo README. Básicamente, crea una instancia de AlignedCollectionViewFlowLayout, especifica la alineación deseada y la asigna a la collectionViewLayoutpropiedad de la vista de su colección :

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

 yourCollectionView.collectionViewLayout = alignedFlowLayout

(También está disponible en Cocoapods ).


Cómo funciona (para celdas alineadas a la izquierda):

El concepto aquí es depender únicamente de la layoutAttributesForItem(at indexPath: IndexPath)función . En el layoutAttributesForElements(in rect: CGRect), simplemente obtenemos las rutas de índice de todas las celdas dentro de recty luego llamamos a la primera función para cada ruta de índice para recuperar los marcos correctos:

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
}

(La copy()función simplemente crea una copia profunda de todos los atributos de diseño en la matriz. Puede buscar en el código fuente para su implementación).

Así que ahora lo único que tenemos que hacer es implementar la layoutAttributesForItem(at indexPath: IndexPath)función correctamente. La superclaseUICollectionViewFlowLayout ya pone el número correcto de celdas en cada línea, por lo que solo tenemos que moverlas a la izquierda dentro de su fila respectiva. La dificultad radica en calcular la cantidad de espacio que necesitamos para desplazar cada celda hacia la izquierda.

Como queremos tener un espaciado fijo entre las celdas, la idea central es simplemente asumir que la celda anterior (la celda a la izquierda de la celda que se presenta actualmente) ya está colocada correctamente. Entonces solo tenemos que agregar el espaciado de celda al maxXvalor del marco de la celda anterior y ese es elorigin.x valor para el marco de la celda actual.

Ahora solo necesitamos saber cuándo hemos llegado al comienzo de una línea, para no alinear una celda al lado de una celda en la línea anterior. (Esto no solo daría como resultado un diseño incorrecto, sino que también sería extremadamente lento). Por lo tanto, necesitamos tener un ancla de recursividad. El enfoque que utilizo para encontrar ese ancla de recursividad es el siguiente:

Para saber si la celda en el índice i está en la misma línea que la celda con el índice i-1 ...

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

... "dibujo" un rectángulo alrededor de la celda actual y lo estiro sobre el ancho de toda la vista de la colección. Como los UICollectionViewFlowLayoutcentros de todas las celdas verticalmente, cada celda en la misma línea debe cruzarse con este rectángulo.

Por lo tanto, simplemente verifico si la celda con índice i-1 se cruza con este rectángulo de línea creado a partir de la celda con índice i .

  • Si se cruza, la celda con el índice i no es la celda más a la izquierda de la línea.
    → Obtenga el marco de la celda anterior (con el índice i − 1 ) y mueva la celda actual a su lado.

  • Si no se cruza, la celda con el índice i es la celda más a la izquierda de la línea.
    → Mueva la celda al borde izquierdo de la vista de colección (sin cambiar su posición vertical).

No publicaré la implementación real de la layoutAttributesForItem(at indexPath: IndexPath)función aquí porque creo que la parte más importante es comprender la idea y siempre puedes verificar mi implementación en el código fuente . (Es un poco más complicado de lo que se explica aquí porque también permito la .rightalineación y varias opciones de alineación vertical. Sin embargo, sigue la misma idea).


Vaya, supongo que esta es la respuesta más larga que he escrito en Stackoverflow. Espero que esto ayude. 😉

Mischa
fuente
Aplico con este escenario stackoverflow.com/questions/56349052/… pero no está funcionando. ¿Me dirías cómo puedo lograr esto?
Nazmul Hasan
Incluso con esto (que es un proyecto realmente impresionante), experimento flasheo en la vista de colección de desplazamiento vertical de mis aplicaciones. Tengo 20 celdas, y en el primer desplazamiento, la primera celda está en blanco y todas las celdas están desplazadas una posición hacia la derecha. Después de un desplazamiento completo hacia arriba y hacia abajo, las cosas se alinean correctamente.
Parth Tamane
Proyecto impresionante! Sería genial verlo habilitado para Carthage.
pjuzeliunas
30

Con Swift 4.1 e iOS 11, según sus necesidades, puede elegir una de las 2 siguientes implementaciones completas para solucionar su problema.


# 1. Alinear a la izquierda el tamaño automático de UICollectionViewCells

La aplicación siguiente muestra cómo utilizar UICollectionViewLayout's layoutAttributesForElements(in:), UICollectionViewFlowLayout' s estimatedItemSize, y UILabel's preferredMaxLayoutWidthcon el fin de alinear las células de tamaño automático izquierda en una 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")
    }

}

Resultado Esperado:

ingrese la descripción de la imagen aquí


# 2. Alinear a la izquierda UICollectionViewCells con tamaño fijo

La implementación de abajo muestra cómo utilizar UICollectionViewLayout's layoutAttributesForElements(in:)y UICollectionViewFlowLayouts itemSizecon el fin de alinear a la izquierda células con tamaño predefinido en unaUICollectionView :

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

}

Resultado Esperado:

ingrese la descripción de la imagen aquí

Imanou Petit
fuente
Al agrupar atributos de celda por fila, ¿se utiliza 10un número arbitrario?
amariduran
25

La solución simple en 2019

Esta es una de esas preguntas deprimentes en las que las cosas han cambiado mucho a lo largo de los años. Ahora es fácil.

Básicamente, solo haces esto:

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

Todo lo que necesita ahora es el código repetitivo:

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
}

Simplemente copie y pegue eso en un UICollectionViewFlowLayout , ya está.

Solución de trabajo completa para copiar y pegar:

Esto es todo:

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

ingrese la descripción de la imagen aquí

Y finalmente...

¡Dale las gracias a @AlexShubin, quien primero aclaró esto!

Fattie
fuente
Oye. Estoy un poco confundido. Agregar espacio para cada cadena al principio y al final todavía muestra mi palabra en la celda del elemento sin el espacio que he agregado. ¿Alguna idea de cómo hacerlo?
Mohamed Lee
Si tiene encabezados de sección en la vista de su colección, esto parece matarlos.
TylerJames
22

La pregunta se ha planteado un tiempo pero no hay respuesta y es una buena pregunta. La respuesta es anular un método en la subclase 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

Según lo recomendado por Apple, obtienes los atributos de diseño de super e iteras sobre ellos. Si es el primero en la fila (definido por su origen.x está en el margen izquierdo), déjelo solo y restablezca la x a cero. Luego, para la primera celda y cada celda, agrega el ancho de esa celda más un margen. Esto pasa al siguiente elemento del ciclo. Si no es el primer elemento, establece su origin.x en el margen calculado en ejecución y agrega nuevos elementos a la matriz.

Mike Sand
fuente
Creo que esto es mucho mejor que la solución de Chris Wagner ( stackoverflow.com/a/15554667/482557 ) en la pregunta vinculada; su implementación no tiene llamadas repetitivas UICollectionViewLayout.
Evan R
1
No parece que esto funcione si la fila tiene un solo elemento porque UICollectionViewFlowLayout lo centra. Puedes consultar el centro para solucionar el problema. Algo comoattribute.center.x == self.collectionView?.bounds.midX
Ryan Poolos
Implementé esto, pero por alguna razón se rompe para la primera sección: la primera celda aparece a la izquierda de la primera fila, y la segunda celda (que no cabe en la misma fila) va a la siguiente fila, pero no a la izquierda alineado. En cambio, su posición x comienza donde termina la primera celda.
Nicolas Miari
@NicolasMiari El bucle decide que es una nueva fila y se restablece leftMargincon el if (attributes.frame.origin.x == self.sectionInset.left) {cheque. Esto supone que el diseño predeterminado ha alineado la celda de la nueva fila para alinearla con sectionInset.left. Si este no es el caso, resultaría el comportamiento que describe. Insertaría un punto de interrupción dentro del bucle y verificaría ambos lados para la celda de la segunda fila justo antes de la comparación. La mejor de las suertes.
Mike Sand
3
Gracias ... Terminé usando UICollectionViewLeftAlignedLayout: github.com/mokagio/UICollectionViewLeftAlignedLayout . Anula un par de métodos más.
Nicolas Miari
9

Tuve el mismo problema. Prueba Cocoapod UICollectionViewLeftAlignedLayout . Solo inclúyalo en su proyecto e inicialícelo así:

UICollectionViewLeftAlignedLayout *layout = [[UICollectionViewLeftAlignedLayout alloc] init];
UICollectionView *leftAlignedCollectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout];
mklb
fuente
Falta un corchete de apertura en su inicialización del diseño allí
RyanOfCourse
Probé las dos respuestas github.com/eroth/ERJustifiedFlowLayout y UICollectionViewLeftAlignedLayout. Solo el segundo produjo el resultado correcto cuando se utilizó estimadoItemSize (tamaño propio).
EckhardN
5

Basándome en la respuesta de Michael Sand , creé una UICollectionViewFlowLayoutbiblioteca de subclases para hacer la justificación horizontal izquierda, derecha o completa (básicamente la predeterminada); también le permite establecer la distancia absoluta entre cada celda. También planeo agregarle justificación central horizontal y justificación vertical.

https://github.com/eroth/ERJustifiedFlowLayout

Evan R
fuente
5

Aquí está la respuesta original en Swift. Todavía funciona muy bien en su mayoría.

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

Excepción: tamaño automático de celdas

Lamentablemente, hay una gran excepción. Si estás usando UICollectionViewFlowLayout's estimatedItemSize. Internamente UICollectionViewFlowLayoutestá cambiando un poco las cosas. No lo he rastreado por completo, pero está claro que está llamando a otros métodos después de las layoutAttributesForElementsInRectceldas de tamaño propio. Por mi prueba y error, descubrí que parece llamar layoutAttributesForItemAtIndexPatha cada celda individualmente durante el tamaño automático con más frecuencia. Esta actualización LeftAlignedFlowLayoutfunciona muy bien con estimatedItemSize. También funciona con celdas de tamaño estático, sin embargo, las llamadas de diseño adicionales me llevan a usar la respuesta original en cualquier momento que no necesite celdas de tamaño automático.

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
fuente
Esto funciona para la primera sección, sin embargo, la etiqueta no cambia de tamaño en la segunda sección hasta que la tabla se desplaza. Mi vista de colección está en la celda de la tabla.
EI Captain v2.0
5

En rápido. Según la respuesta de Michaels

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

Basado en todas las respuestas, cambio un poco y me funciona bien

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

Si alguno de ustedes tiene un problema, algunas de las celdas que están a la derecha de la vista de colección exceden los límites de la vista de colección . Entonces por favor use esto -

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

Utilice INT en lugar de comparar valores CGFloat .

niku
fuente
3

Basado en las respuestas aquí, pero se corrigieron fallas y problemas de alineación cuando la vista de su colección también tiene encabezados o pies de página. Alinear solo celdas a la izquierda:

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

Gracias por la respuesta de Michael Sand . Lo modifiqué a una solución de varias filas (la misma alineación superior y de cada fila) que es Alineación izquierda, con un espaciado uniforme para cada elemento.

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

Aquí está mi viaje para encontrar el mejor código que funcione con Swift 5. He unido un par de respuestas de este hilo y algunos otros hilos para resolver advertencias y problemas que enfrenté. Recibí una advertencia y un comportamiento anormal al desplazarme por la vista de mi colección. La consola imprime lo siguiente:

Es probable que esto ocurra porque el diseño de flujo "xyz" está modificando los atributos devueltos por UICollectionViewFlowLayout sin copiarlos.

Otro problema al que me enfrenté es que algunas celdas largas se recortan en el lado derecho de la pantalla. Además, estaba configurando las inserciones de sección y el mínimoInteritemSpacing en las funciones de delegado que dieron como resultado que los valores no se reflejaran en la clase personalizada. La solución para eso fue establecer esos atributos en una instancia del diseño antes de aplicarlo a la vista de mi colección.

Así es como usé el diseño para la vista de mi colección:

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)

Aquí está la clase de diseño de flujo

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
fuente
Me salvaste el día
David 'mArm' Ansermot
0

El problema con UICollectionView es que intenta ajustar automáticamente las celdas en el área disponible. He hecho esto definiendo primero el número de filas y columnas y luego definiendo el tamaño de celda para esa fila y columna

1) Para definir secciones (filas) de mi UICollectionView:

(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView

2) Para definir el número de elementos en una sección. Puede definir un número diferente de elementos para cada sección. puede obtener el número de sección usando el parámetro 'sección'.

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

3) Definir el tamaño de celda para cada sección y fila por separado. Puede obtener el número de sección y el número de fila utilizando el parámetro 'indexPath', es decir, [indexPath section]para el número de sección y el número [indexPath row]de fila.

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

4) Luego, puede mostrar sus celdas en filas y secciones usando:

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

NOTA: En UICollectionView

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

La respuesta de Mike Sand es buena, pero había experimentado algunos problemas con este código (como una celda larga recortada). Y nuevo código:

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

Editó la respuesta de Ángel García Olloqui al respeto minimumInteritemSpacingde los delegados collectionView(_:layout:minimumInteritemSpacingForSectionAt:), si la implementa.

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

El código anterior funciona para mí. Me gustaría compartir el código Swift 3.0 respectivo.

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
fuente