ios objective-c uicollectionview uicollectionviewlayout

ios - Left Align Cells en UICollectionView



uicollectionview layout (15)

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

extiende las celdas a través de la línea con igual relleno. Esto sucede como se esperaba, excepto que quiero justificarlos a la izquierda, y codificar un ancho de relleno.

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


Aquí está la respuesta original en Swift. Todavía funciona genial 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: Autosizing Celdas

Hay una gran excepción tristemente. Si está usando UICollectionViewFlowLayout estimatedItemSize UICollectionViewFlowLayout . Internamente, UICollectionViewFlowLayout está cambiando un poco las cosas. No lo he rastreado del todo, pero está claro que llama a otros métodos después de layoutAttributesForElementsInRect al mismo tiempo que el tamaño de las celdas. A partir de mi prueba y error, encontré que parece llamar a layoutAttributesForItemAtIndexPath para cada celda individualmente durante el autosizing con más frecuencia. Este LeftAlignedFlowLayout actualizado funciona 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 utilizar la respuesta original en cualquier momento que no necesite autosizing cells.

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


Basado en las respuestas aquí, pero arregla bloqueos y problemas de alineación cuando tu vista de colección también tiene encabezados o pies de página. Alineando solo celdas:

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


Basado en todas las respuestas, cambio un poco y funciona bien para mí

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 }


Con Swift 4.1 e iOS 11, de acuerdo con sus necesidades, puede elegir una de las dos siguientes implementaciones completas para solucionar su problema.

# 1. Alineación izquierda UICollectionViewCell s

La implementación a continuación muestra cómo usar UICollectionViewLayout ''s layoutAttributesForElements(in:) , UICollectionViewFlowLayout ''s estimatedItemSize y UILabel ''s preferredMaxLayoutWidth para alinear a la izquierda las células de autoevaluación en 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() title = "Center cells" 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:

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

La implementación a continuación muestra cómo usar UICollectionViewLayout ''s layoutAttributesForElements(in:) y UICollectionViewFlowLayout ''s itemSize para alinear a la izquierda las celdas con un tamaño predefinido en un 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() title = "Center cells" 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:


Editado la respuesta de Angel García Olloqui para respetar minimumInteritemSpacing from delegate''s collectionView(_:layout:minimumInteritemSpacingForSectionAt:) , si lo 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 }


El problema con UICollectionView es que intenta ajustar automáticamente las celdas en el área disponible. Lo he hecho 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 la cantidad de elementos en una sección. Puede definir diferentes cantidades de artículos 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) Para 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 [indexPath row] para el número 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


En rápido. De acuerdo con 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 }


Gracias por la respuesta de Michael Sand . Lo modifiqué a una solución de múltiples filas (la misma alineación Top y de cada fila) que es Alineación a la Izquierda, incluso espaciando a cada ítem.

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


Hay muchas ideas geniales incluidas en las respuestas a esta pregunta. Sin embargo, la mayoría de ellos tienen algunos inconvenientes:

  • Las soluciones que no verifican el valor y de la celda solo funcionan para diseños de una sola línea . Fallan en diseños de vista de colección con varias líneas.
  • Las soluciones que revisan el valor y , como la respuesta de Ángel García Olloqui, solo funcionan si todas las celdas tienen la misma altura . Fallan para las celdas con una altura variable.
  • La mayoría de las soluciones solo anulan la función layoutAttributesForElements(in rect: CGRect) . 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 correctos 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 autoevaluación al configurar el layoutItemSize del diseño de la vista de colección. Los documentos de Apple dicen:

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

  • Muchas soluciones también hacen suposiciones sobre el parámetro rect que se pasa a la función layoutAttributesForElements(in rect: CGRect) . Por ejemplo, muchos se basan en la suposición de que el rect siempre comienza al comienzo de una nueva línea, 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 espera en cada situación.

AlignedCollectionViewFlowLayout

Para abordar estos problemas, he creado una subclase UICollectionViewFlowLayout 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 :

o ➡︎ derecha :

y además ofrece opciones para alinear verticalmente 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 directo y se explica en el archivo README. Básicamente, crea una instancia de AlignedCollectionViewFlowLayout , especifica la alineación deseada y la asigna a la propiedad collectionViewLayout su vista de collectionViewLayout :

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 confiar únicamente en la función layoutAttributesForItem(at indexPath: IndexPath) . En layoutAttributesForElements(in rect: CGRect) simplemente obtenemos las rutas de índice de todas las celdas dentro del rect y luego llamamos a la primera función para cada ruta de índice para recuperar las tramas correctas:

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 función copy() simplemente crea una copia profunda de todos los atributos de diseño de la matriz. Puede buscar el código fuente para su implementación).

Entonces, lo único que tenemos que hacer es implementar layoutAttributesForItem(at indexPath: IndexPath) el layoutAttributesForItem(at indexPath: IndexPath) . La UICollectionViewFlowLayout ya coloca el número correcto de celdas en cada línea, por lo que solo debemos desplazarlas a la izquierda dentro de su respectiva fila. La dificultad radica en calcular la cantidad de espacio que necesitamos para desplazar cada celda hacia la izquierda.

Como queremos tener un espacio fijo entre las celdas, la idea central es simplemente suponer que la celda anterior (la celda a la izquierda de la celda que se encuentra actualmente distribuida) ya está posicionada correctamente. Entonces solo tenemos que agregar el espacio entre celdas al valor maxX del maxX de la celda anterior y ese es el valor de origin.x para el cuadro de la celda actual.

Ahora solo necesitamos saber cuándo hemos llegado al comienzo de una línea, para que no alineemos 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, debemos tener un ancla de recursión. El enfoque que uso para encontrar ese ancla de recursión es el siguiente:

Para saber si la celda del í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 | | +---------+----------------------------------------------------------------+---------+

... "Dibujé" un rectángulo alrededor de la celda actual y la estiré sobre el ancho de toda la vista de la colección. Como UICollectionViewFlowLayout centra todas las celdas verticalmente, todas las celdas de la misma línea deben 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 índice i no es la celda más a la izquierda en la línea.
    → Obtenga el marco de la celda anterior (con el índice i-1 ) y mueva la celda actual al lado.

  • Si no se cruza, la celda con í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 actual de la función layoutAttributesForItem(at indexPath: IndexPath) 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 que el explicado aquí porque también permito .right alineación .right y varias opciones de alineación vertical. Sin embargo, sigue la misma idea).

Wow, supongo que esta es la respuesta más larga que he escrito sobre . Espero que esto ayude. 😉


La pregunta ha sido 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 en el margen izquierdo), lo deja en paz y restablece la x a cero. Luego, para la primera celda y cada celda, agregue el ancho de esa celda más algún margen. Esto pasa al siguiente elemento en el ciclo. Si no es el primer elemento, establezca su origen.x en el margen calculado en ejecución y agregue nuevos elementos a la matriz.


La respuesta de Mike Sand es buena, pero he tenido 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; }


Las otras soluciones aquí no funcionan correctamente cuando la línea está compuesta solo por 1 elemento o son demasiado complicadas.

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

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

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


Sobre la base de la respuesta de Michael Sand , creé una biblioteca UICollectionViewFlowLayout subclasificada 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. Planeo agregar también justificación de centro horizontal y justificación vertical.

github.com/eroth/ERJustifiedFlowLayout


Tuve el mismo problema, pruebe el Cocoapod github.com/mokagio/UICollectionViewLeftAlignedLayout . Simplemente inclúyelo en tu proyecto e inicialízalo así:

UICollectionViewLeftAlignedLayout *layout = [[UICollectionViewLeftAlignedLayout alloc] init]; UICollectionView *leftAlignedCollectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout];


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