¿Cómo se determina el espaciado entre celdas en UICollectionView flowLayout?

Tengo un UICollectionView con un diseño de flujo y cada celda es un cuadrado. ¿Cómo determino el espacio entre cada celda en cada fila? Parece que no puedo encontrar la configuración adecuada para esto. Veo que hay un mínimo de espaciado de atributos en el archivo de punta para una vista de colección, pero configuré esto en 0 y las células ni siquiera se pegan.

enter image description here

¿Alguna otra idea?

Actualización: versión rápida de esta respuesta: https://github.com/fanpyi/UICollectionViewLeftAlignedLayout-Swift


Tomando la delantera de @ matt modifiqué su código para asegurar que los artículos SIEMPRE queden alineados. Descubrí que si un artículo terminaba en una línea por sí mismo, se centraría en el diseño del flujo. Hice los siguientes cambios para abordar este problema.

Esta situación solo ocurrirá si tiene celdas que varían en ancho, lo que podría generar un diseño como el siguiente. La última línea siempre a la izquierda se alinea debido al comportamiento de UICollectionViewFlowLayout , el problema radica en los elementos que están solos en cualquier línea, excepto la última.

Con el código de @ matt que estaba viendo.

enter image description here

En ese ejemplo, vemos que las células se centran si terminan en la línea por sí mismas. El siguiente código asegura que su vista de colección se vería así.

enter image description here

 #import "CWDLeftAlignedCollectionViewFlowLayout.h" const NSInteger kMaxCellSpacing = 9; @implementation CWDLeftAlignedCollectionViewFlowLayout - (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 + kMaxCellSpacing; 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; } @end 

Para obtener un espaciado entre sitios máximo, la subclase UICollectionViewFlowLayout y sobrescribe layoutAttributesForElementsInRect: y layoutAttributesForItemAtIndexPath:

Por ejemplo, un problema común es este: las filas de una vista de colección están justificadas a la derecha e izquierda, a excepción de la última línea que está justificada a la izquierda. Digamos que queremos que todas las líneas estén justificadas a la izquierda, de modo que el espacio entre ellas sea, digamos, 10 puntos. Aquí hay una manera fácil (en su subclase UICollectionViewFlowLayout):

 - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { NSArray* arr = [super layoutAttributesForElementsInRect:rect]; for (UICollectionViewLayoutAttributes* atts in arr) { if (nil == atts.representedElementKind) { NSIndexPath* ip = atts.indexPath; atts.frame = [self layoutAttributesForItemAtIndexPath:ip].frame; } } return arr; } - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { UICollectionViewLayoutAttributes* atts = [super layoutAttributesForItemAtIndexPath:indexPath]; if (indexPath.item == 0) // degenerate case 1, first item of section return atts; NSIndexPath* ipPrev = [NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section]; CGRect fPrev = [self layoutAttributesForItemAtIndexPath:ipPrev].frame; CGFloat rightPrev = fPrev.origin.x + fPrev.size.width + 10; if (atts.frame.origin.x < = rightPrev) // degenerate case 2, first item of line return atts; CGRect f = atts.frame; f.origin.x = rightPrev; atts.frame = f; return atts; } 

La razón por la que esto es tan fácil es que no realizamos realmente el trabajo pesado del diseño; estamos aprovechando el trabajo de diseño que UICollectionViewFlowLayout ya ha hecho por nosotros. Ya ha decidido cuántos elementos van en cada línea; solo estamos leyendo esas líneas y empujando los elementos juntos, si entiendes lo que quiero decir.

Hay algunas cosas a considerar:

  1. Intente cambiar el espaciado mínimo en IB, pero deje el cursor en ese campo. Observe que Xcode no marca inmediatamente el documento como cambiado. Sin embargo, cuando hace clic en un campo diferente, Xcode se da cuenta de que el documento ha cambiado y lo marca en el navegador de archivos. Por lo tanto, asegúrese de tabular o hacer clic en un campo diferente después de hacer un cambio.

  2. Guarde su archivo storyboard / xib después de hacer un cambio, y asegúrese de reconstruir la aplicación. No es difícil perderse ese paso, y luego te estás rascando la cabeza preguntándote por qué los cambios no parecían tener ningún efecto.

  3. UICollectionViewFlowLayout tiene una propiedad minimumInteritemSpacing , que es lo que está configurando en IB. Pero el delegado de la colección también puede tener un método para determinar el espaciado entre elementos . Ese método prevalece sobre la propiedad del diseño, por lo que si lo implementa en su delegado no se usará la propiedad de su diseño.

  4. Recuerde que el espaciado allí es un espacio mínimo . El diseño usará ese número (ya sea que provenga de la propiedad o del método delegado) como el espacio más pequeño permitido, pero puede usar un espacio más grande si tiene espacio sobrante en la línea. Entonces, si, por ejemplo, establece el espaciado mínimo en 0, aún puede ver algunos píxeles entre los elementos. Si desea tener más control sobre cómo se espacian los elementos, probablemente debería usar un diseño diferente (posiblemente uno de su propia creación).

Un poco de matemáticas hace el truco más fácilmente. El código escrito por Chris Wagner es horrible porque llama a los atributos de diseño de cada elemento anterior. Así que cuanto más se desplaza, más lento es …

Simplemente use módulo como este (estoy usando mi valor mínimoInterEmSpacing como un valor máximo también):

 - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { UICollectionViewLayoutAttributes* currentItemAttributes = [super layoutAttributesForItemAtIndexPath:indexPath]; NSInteger numberOfItemsPerLine = floor([self collectionViewContentSize].width / [self itemSize].width); if (indexPath.item % numberOfItemsPerLine != 0) { NSInteger cellIndexInLine = (indexPath.item % numberOfItemsPerLine); CGRect itemFrame = [currentItemAttributes frame]; itemFrame.origin.x = ([self itemSize].width * cellIndexInLine) + ([self minimumInteritemSpacing] * cellIndexInLine); currentItemAttributes.frame = itemFrame; } return currentItemAttributes; } 

Una forma fácil de justificar a la izquierda es modificar layoutAttributesForElementsInRect: en su subclase de UICollectionViewFlowLayout:

 - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { NSArray *allLayoutAttributes = [super layoutAttributesForElementsInRect:rect]; CGRect prevFrame = CGRectMake(-FLT_MAX, -FLT_MAX, 0, 0); for (UICollectionViewLayoutAttributes *layoutAttributes in allLayoutAttributes) { //fix blur CGRect theFrame = CGRectIntegral(layoutAttributes.frame); //left justify if(prevFrame.origin.x > -FLT_MAX && prevFrame.origin.y >= theFrame.origin.y && prevFrame.origin.y < = theFrame.origin.y) //workaround for float == warning { theFrame.origin.x = prevFrame.origin.x + prevFrame.size.width + EXACT_SPACE_BETWEEN_ITEMS; } prevFrame = theFrame; layoutAttributes.frame = theFrame; } return allLayoutAttributes; } 

La versión rápida de la solución Chris.

 class PazLeftAlignedCollectionViewFlowLayout : UICollectionViewFlowLayout { var maxCellSpacing = 14.0 override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? { if var attributesToReturn = super.layoutAttributesForElementsInRect(rect) as? Array { for attributes in attributesToReturn { if attributes.representedElementKind == nil { let indexPath = attributes.indexPath attributes.frame = self.layoutAttributesForItemAtIndexPath(indexPath).frame; } } return attributesToReturn; } return super.layoutAttributesForElementsInRect(rect) } override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! { let currentItemAttributes = super.layoutAttributesForItemAtIndexPath(indexPath) if let collectionViewFlowLayout = self.collectionView?.collectionViewLayout as? UICollectionViewFlowLayout { let sectionInset = collectionViewFlowLayout.sectionInset if (indexPath.item == 0) { // first item of section var frame = currentItemAttributes.frame; frame.origin.x = sectionInset.left; // first item of the section should always be left aligned currentItemAttributes.frame = frame; return currentItemAttributes; } let previousIndexPath = NSIndexPath(forItem:indexPath.item-1, inSection:indexPath.section) let previousFrame = self.layoutAttributesForItemAtIndexPath(previousIndexPath).frame; let previousFrameRightPoint = Double(previousFrame.origin.x) + Double(previousFrame.size.width) + self.maxCellSpacing let currentFrame = currentItemAttributes.frame var width : CGFloat = 0.0 if let collectionViewWidth = self.collectionView?.frame.size.width { width = collectionViewWidth } let strecthedCurrentFrame = CGRectMake(0, currentFrame.origin.y, 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 var frame = currentItemAttributes.frame; frame.origin.x = sectionInset.left; // first item on the line should always be left aligned currentItemAttributes.frame = frame; return currentItemAttributes; } var frame = currentItemAttributes.frame; frame.origin.x = CGFloat(previousFrameRightPoint) currentItemAttributes.frame = frame; } return currentItemAttributes; } } 

Para usarlo haz lo siguiente:

  override func viewDidLoad() { super.viewDidLoad() self.collectionView.collectionViewLayout = self.layout } var layout : PazLeftAlignedCollectionViewFlowLayout { var layout = PazLeftAlignedCollectionViewFlowLayout() layout.itemSize = CGSizeMake(220.0, 230.0) layout.minimumLineSpacing = 12.0 return layout } 

Una versión más rápida y limpia para las personas interesadas, basada en la respuesta de Chris Wagner:

 class AlignLeftFlowLayout: UICollectionViewFlowLayout { var maximumCellSpacing = CGFloat(9.0) override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? { let attributesToReturn = super.layoutAttributesForElementsInRect(rect) as? [UICollectionViewLayoutAttributes] for attributes in attributesToReturn ?? [] { if attributes.representedElementKind == nil { attributes.frame = self.layoutAttributesForItemAtIndexPath(attributes.indexPath).frame } } return attributesToReturn } override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! { let curAttributes = super.layoutAttributesForItemAtIndexPath(indexPath) let sectionInset = (self.collectionView?.collectionViewLayout as UICollectionViewFlowLayout).sectionInset if indexPath.item == 0 { let f = curAttributes.frame curAttributes.frame = CGRectMake(sectionInset.left, f.origin.y, f.size.width, f.size.height) return curAttributes } let prevIndexPath = NSIndexPath(forItem: indexPath.item-1, inSection: indexPath.section) let prevFrame = self.layoutAttributesForItemAtIndexPath(prevIndexPath).frame let prevFrameRightPoint = prevFrame.origin.x + prevFrame.size.width + maximumCellSpacing let curFrame = curAttributes.frame let stretchedCurFrame = CGRectMake(0, curFrame.origin.y, self.collectionView!.frame.size.width, curFrame.size.height) if CGRectIntersectsRect(prevFrame, stretchedCurFrame) { curAttributes.frame = CGRectMake(prevFrameRightPoint, curFrame.origin.y, curFrame.size.width, curFrame.size.height) } else { curAttributes.frame = CGRectMake(sectionInset.left, curFrame.origin.y, curFrame.size.width, curFrame.size.height) } return curAttributes } } 

Solución Clean Swift, de una historia de evolución:

  1. hubo respuesta mate
  2. había elementos solitarios Chris Wagner arreglar
  3. hubo mokagio sectionInset y minimumInteritemSpacing improvement
  4. había fanpyi versión Swift
  5. ahora aquí hay una versión simplificada y limpia de la mía:
 open class UICollectionViewLeftAlignedLayout: UICollectionViewFlowLayout { open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return super.layoutAttributesForElements(in: rect)?.map { $0.representedElementKind == nil ? layoutAttributesForItem(at: $0.indexPath)! : $0 } } open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { guard let currentItemAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes, collectionView != nil else { // should never happen return nil } // if the current frame, once stretched to the full row intersects the previous frame then they are on the same row if indexPath.item != 0, let previousFrame = layoutAttributesForItem(at: IndexPath(item: indexPath.item - 1, section: indexPath.section))?.frame, currentItemAttributes.frame.intersects(CGRect(x: -.infinity, y: previousFrame.origin.y, width: .infinity, height: previousFrame.size.height)) { // the next item on a line currentItemAttributes.frame.origin.x = previousFrame.origin.x + previousFrame.size.width + evaluatedMinimumInteritemSpacingForSection(at: indexPath.section) } else { // the first item on a line currentItemAttributes.frame.origin.x = evaluatedSectionInsetForSection(at: indexPath.section).left } return currentItemAttributes } func evaluatedMinimumInteritemSpacingForSection(at section: NSInteger) -> CGFloat { return (collectionView?.delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView!, layout: self, minimumInteritemSpacingForSectionAt: section) ?? minimumInteritemSpacing } func evaluatedSectionInsetForSection(at index: NSInteger) -> UIEdgeInsets { return (collectionView?.delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView!, layout: self, insetForSectionAt: index) ?? sectionInset } } 

Uso: el espacio entre elementos está determinado por collectionView (_:layout:minimumInteritemSpacingForSectionAt:) .

Lo puse en github, https://github.com/Coeur/UICollectionViewLeftAlignedLayout , donde en realidad agregué una función de compatibilidad con ambas direcciones de desplazamiento (horizontal y vertical).

Aquí está para NSCollectionViewFlowLayout

 class LeftAlignedCollectionViewFlowLayout: NSCollectionViewFlowLayout { var maximumCellSpacing = CGFloat(2.0) override func layoutAttributesForElementsInRect(rect: NSRect) -> [NSCollectionViewLayoutAttributes] { let attributesToReturn = super.layoutAttributesForElementsInRect(rect) for attributes in attributesToReturn ?? [] { if attributes.representedElementKind == nil { attributes.frame = self.layoutAttributesForItemAtIndexPath(attributes.indexPath!)!.frame } } return attributesToReturn } override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> NSCollectionViewLayoutAttributes? { let curAttributes = super.layoutAttributesForItemAtIndexPath(indexPath) let sectionInset = (self.collectionView?.collectionViewLayout as! NSCollectionViewFlowLayout).sectionInset if indexPath.item == 0 { let f = curAttributes!.frame curAttributes!.frame = CGRectMake(sectionInset.left, f.origin.y, f.size.width, f.size.height) return curAttributes } let prevIndexPath = NSIndexPath(forItem: indexPath.item-1, inSection: indexPath.section) let prevFrame = self.layoutAttributesForItemAtIndexPath(prevIndexPath)!.frame let prevFrameRightPoint = prevFrame.origin.x + prevFrame.size.width + maximumCellSpacing let curFrame = curAttributes!.frame let stretchedCurFrame = CGRectMake(0, curFrame.origin.y, self.collectionView!.frame.size.width, curFrame.size.height) if CGRectIntersectsRect(prevFrame, stretchedCurFrame) { curAttributes!.frame = CGRectMake(prevFrameRightPoint, curFrame.origin.y, curFrame.size.width, curFrame.size.height) } else { curAttributes!.frame = CGRectMake(sectionInset.left, curFrame.origin.y, curFrame.size.width, curFrame.size.height) } return curAttributes } } 

El “problema” con UICollectionViewFlowLayout es que aplica una alineación justificada a las celdas: la primera celda en una fila se alinea a la izquierda, la última celda en una fila se alinea a la derecha y todas las otras celdas intermedias se distribuyen uniformemente con un igual espaciado que es mayor que el minimumInteritemSpacing .

Ya hay muchas respuestas excelentes para esta publicación que resuelven este problema subclasificando UICollectionViewFlowLayout . Como resultado, obtiene un diseño que alinea las celdas restantes . Otra solución válida para distribuir las celdas con un espaciado constante es alinear las celdas a la derecha .

AlignedCollectionViewFlowLayout

También creé una subclase UICollectionViewFlowLayout que sigue una idea similar sugerida por Matt y Chris Wagner que puede alinear las celdas

⬅︎ izquierda :

Diseño alineado a la izquierda

o ➡︎ derecha :

Diseño alineado a la derecha

Simplemente puede descargarlo desde aquí, agregar el archivo de diseño a su proyecto y establecer AlignedCollectionViewFlowLayout como la clase de diseño de su vista de colección:
https://github.com/mischa-hildebrand/AlignedCollectionViewFlowLayout

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

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

El concepto aquí es verificar si la celda actual con índice iy la celda anterior con el índice i-1 ocupan la misma línea.

  • Si no lo hacen, 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).
  • Si lo hacen, 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.

Para celdas alineadas a la derecha …

… haces lo mismo al revés, es decir, que marques la siguiente celda con el índice i + 1 en su lugar.

una base de versión rápida en mokagio: https://github.com/fanpyi/UICollectionViewLeftAlignedLayout-Swift

 class UICollectionViewLeftAlignedLayout: UICollectionViewFlowLayout { override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let attributesToReturn = super.layoutAttributesForElementsInRect(rect) if let attributesToReturn = attributesToReturn { for attributes in attributesToReturn { if attributes.representedElementKind == nil { let indexpath = attributes.indexPath if let attr = layoutAttributesForItemAtIndexPath(indexpath) { attributes.frame = attr.frame } } } } return attributesToReturn } override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? { if let currentItemAttributes = super.layoutAttributesForItemAtIndexPath(indexPath){ let sectionInset = self.evaluatedSectionInsetForItemAtIndex(indexPath.section) let isFirstItemInSection = indexPath.item == 0; let layoutWidth = CGRectGetWidth(self.collectionView!.frame) - sectionInset.left - sectionInset.right; if (isFirstItemInSection) { currentItemAttributes.leftAlignFrameWithSectionInset(sectionInset) return currentItemAttributes } let previousIndexPath = NSIndexPath(forItem: indexPath.item - 1, inSection: indexPath.section) let previousFrame = layoutAttributesForItemAtIndexPath(previousIndexPath)?.frame ?? CGRectZero let previousFrameRightPoint = previousFrame.origin.x + previousFrame.width let currentFrame = currentItemAttributes.frame; let strecthedCurrentFrame = CGRectMake(sectionInset.left, currentFrame.origin.y, layoutWidth, currentFrame.size.height) // if the current frame, once left aligned to the left and stretched to the full collection view // widht intersects the previous frame then they are on the same line let isFirstItemInRow = !CGRectIntersectsRect(previousFrame, strecthedCurrentFrame) if (isFirstItemInRow) { // make sure the first item on a line is left aligned currentItemAttributes.leftAlignFrameWithSectionInset(sectionInset) return currentItemAttributes } var frame = currentItemAttributes.frame; frame.origin.x = previousFrameRightPoint + evaluatedMinimumInteritemSpacingForSectionAtIndex(indexPath.section) currentItemAttributes.frame = frame; return currentItemAttributes; } return nil } func evaluatedMinimumInteritemSpacingForSectionAtIndex(sectionIndex:Int) -> CGFloat { if let delegate = self.collectionView?.delegate as? UICollectionViewDelegateFlowLayout { if delegate.respondsToSelector("collectionView:layout:minimumInteritemSpacingForSectionAtIndex:") { return delegate.collectionView!(self.collectionView!, layout: self, minimumInteritemSpacingForSectionAtIndex: sectionIndex) } } return self.minimumInteritemSpacing } func evaluatedSectionInsetForItemAtIndex(index: Int) ->UIEdgeInsets { if let delegate = self.collectionView?.delegate as? UICollectionViewDelegateFlowLayout { if delegate.respondsToSelector("collectionView:layout:insetForSectionAtIndex:") { return delegate.collectionView!(self.collectionView!, layout: self, insetForSectionAtIndex: index) } } return self.sectionInset } }