Left Align Cells en UICollectionView

Estoy usando una UICollectionView en mi proyecto, donde hay varias celdas de diferentes anchos en una línea. De acuerdo con: 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 y el código duro un ancho de relleno.

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

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

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

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 objective:

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

La pregunta ha sido un poco de time, pero no hay respuesta y es una buena pregunta. La respuesta es anular un método en la subclass 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 attributes de layout de super e itera sobre ellos. Si es el primero de la fila (definido por su origen.x en el margen izquierdo), déjelo solo y restablezca x en cero. Luego, para la primera celda y cada celda, agregue el ancho de esa celda más un margen. Esto pasa al siguiente elemento en el bucle. Si no es el primer elemento, establece su origen.x en el margen calculado en ejecución y agrega nuevos elementos a la matriz.

Tuve el mismo problema, testing el Cocoapod UICollectionViewLeftAlignedLayout . Simplemente incluyalo en tu proyecto e inicialízalo así:

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

En rapido De acuerdo con Michaels respuesta

 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 } 

Sobre la base de la respuesta de Michael Sand , he creado una subclass UICollectionViewFlowLayout para hacer la justificación horizontal izquierda, derecha o completa (básicamente la pnetworkingeterminada); también le permite establecer la distancia absoluta entre cada celda. Planeo agregar también una justificación central horizontal y una justificación vertical.

https://github.com/eroth/ERJustifiedFlowLayout

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 comtestingn el valor de la celda solo funcionan para layouts de una sola línea . Fallen para layouts de vista de colección con múltiples líneas.
  • Las soluciones que comtestingn el valor y la respuesta de Angel García Olloqui solo funcionan si todas las celdas tienen la misma altura . Fallen para celdas con una altura variable.
  • La mayoría de las soluciones solo anulan la function layoutAttributesForElements(in rect: CGRect) . No reemplazan layoutAttributesForItem(at indexPath: IndexPath) . Este es un problema porque la vista de colección llama periódicamente a esta última function para recuperar los attributes de layout para una ruta de índice particular. Si no devuelve los attributes adecuados de esa function, 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 automático al establecer el estimatedItemSize del layout de la vista de colección. El estado de los documentos de Apple :

    Se espera que cada object de layout personalizado implemente el método layoutAttributesForItemAtIndexPath:

  • Muchas soluciones también hacen suposiciones sobre el parámetro rect que se pasa a la function layoutAttributesForElements(in rect: CGRect) . Por ejemplo, muchos se basan en la suposition 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 subclass 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 :

Diseño alineado a la izquierda

o ➡︎ derecho :

Diseño alineado a la derecha

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

Simplemente puede downloadlo aquí:

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

El uso es sencillo y se explica en el file README. Básicamente, crea una instancia de AlignedCollectionViewFlowLayout , especifique la alignment deseada y asígnela a la propiedad collectionViewLayout de su 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 function layoutAttributesForItem(at indexPath: IndexPath) . En el layoutAttributesForElements(in rect: CGRect) , simplemente obtenemos las routes de índice de todas las celdas dentro del rect y luego llamamos a la primera function para cada ruta de índice para recuperar los cuadros 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 function copy() simplemente crea una copy profunda de todos los attributes de layout en la matriz. Puede search en el código fuente para su implementación.)

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

Como queremos tener un espacio fijo entre las celdas, la idea central es asumir que la celda anterior (la celda a la izquierda de la celda que se encuentra actualmente) ya está colocada correctamente. Luego 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 marco 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 layout incorrecto sino que también sería muy lagoso). Por lo tanto, necesitamos tener un ancla de recursion. El enfoque que uso para encontrar ese anclaje de recursion es el siguiente:

Para averiguar 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 alnetworkingedor de la celda actual y lo estiro 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 controlo 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 izquierda de 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 izquierda de la línea.
    → Mueva la celda al borde izquierdo de la vista de la colección (sin cambiar su position vertical).

No publicaré la implementación real de la function 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 explicarlo aquí porque también permito la alignment .right y varias opciones de alignment vertical. Sin embargo, sigue la misma idea).


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

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: autocaptando celdas

Hay una gran exception tristemente. Si está utilizando UICollectionViewFlowLayout de UICollectionViewFlowLayout . Internamente UICollectionViewFlowLayout está cambiando un poco las cosas. No lo he rastreado por completo, pero está claro que llama a otros methods después de layoutAttributesForElementsInRect mientras las celdas de tamaño automático. De mi ensayo 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 . Funciona también con celdas de tamaño estático, sin embargo, las llamadas de layout adicionales me llevan a usar la respuesta original en cualquier momento en que no necesito células autodiseñadas.

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

Gracias por la respuesta de Michael Sand . Lo modifiqué a una solución de múltiples filas (la misma alignment Superior y de cada fila) que es Alineación a la Izquierda, incluso espaciando a 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; } 

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 } 

El problema con UICollectionView es que intenta ajustarse automáticamente a las celdas en el área disponible. Lo he hecho primero definiendo 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 artículos en una sección. Puede definir diferentes numbers de elementos para cada sección. Puede get 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 get el número de la sección y el número de la 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 

La respuesta de Mike Sand es buena, pero experimenté algunos problemas con este código (Me gusta la 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; } 

minimumInteritemSpacing la respuesta de Angel García Olloqui para respetar el minimumInteritemSpacing de minimumInteritemSpacing desde la collectionView(_:layout:minimumInteritemSpacingForSectionAt:) del delegado. Ver 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 } 

Según las respuestas aquí, pero se solucionaron los lockings y los problemas de alignment cuando la vista de colección también recibió encabezados o pies de página. Alinear las celdas solo izquierdas:

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

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