Especificar una dimensión de celdas en UICollectionView utilizando diseño automático

En iOS 8, UICollectionViewFlowLayout admite el cambio de tamaño de las celdas automáticamente en función de su propio tamaño de contenido. Esto cambia el tamaño de las celdas en ancho y alto de acuerdo a su contenido.

¿Es posible especificar un valor fijo para el ancho (o alto) de todas las celdas y permitir que las otras dimensiones cambien de tamaño?

Para un ejemplo simple, considere una etiqueta de varias líneas en una celda con restricciones posicionándola a los lados de la celda. La etiqueta de varias líneas podría cambiar de tamaño de diferentes maneras para acomodar el texto. La celda debe llenar el ancho de la vista de colección y ajustar su altura en consecuencia. En cambio, las celdas se clasifican al azar e incluso provoca un locking cuando el tamaño de la celda es mayor que la dimensión no desplazable de la vista de colección.


iOS 8 presenta el método systemLayoutSizeFittingSize: withHorizontalFittingPriority: verticalFittingPriority: para cada celda en la vista de colección, el diseño llama a este método en la celda, pasando el tamaño estimado. Lo que tendría sentido para mí sería anular este método en la celda, pasar el tamaño que se da y establecer la restricción horizontal a requerida y una baja prioridad a la restricción vertical. De esta forma, el tamaño horizontal se fija al valor establecido en el diseño y el tamaño vertical puede ser flexible.

Algo como esto:

 - (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes { UICollectionViewLayoutAttributes *attributes = [super preferredLayoutAttributesFittingAttributes:layoutAttributes]; attributes.size = [self systemLayoutSizeFittingSize:layoutAttributes.size withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel]; return attributes; } 

Los tamaños devueltos por este método, sin embargo, son completamente extraños. La documentación sobre este método no está muy clara para mí y menciona el uso de las constantes UILayoutFittingCompressedSize UILayoutFittingExpandedSize que solo representan un tamaño cero y uno bastante grande.

¿El parámetro de size de este método es realmente solo una forma de pasar dos constantes? ¿No hay forma de lograr el comportamiento que espero obtener la altura adecuada para un tamaño dado?


Soluciones Alternativas

1) Agregar restricciones que especificarán un ancho específico para la celda logra el diseño correcto. Esta es una solución deficiente porque esa restricción debe establecerse en el tamaño de la vista de colección de la celda a la que no se hace referencia de forma segura. El valor de esa restricción podría pasarse cuando la celda está configurada, pero eso también parece completamente contrario a la intuición. Esto también es incómodo porque agregar restricciones directamente a una celda o su vista de contenido está causando muchos problemas.

2) Use una vista de tabla. Las vistas de tabla funcionan de esta manera, ya que las celdas tienen un ancho fijo, pero esto no se ajustaría a otras situaciones, como un diseño de iPad con celdas de ancho fijo en varias columnas.

Parece que lo que está pidiendo es una forma de usar UICollectionView para producir un diseño como UITableView. Si eso es lo que realmente quieres, la forma correcta de hacerlo es con una subclase UICollectionViewLayout personalizada (tal vez algo como SBTableLayout ).

Por otro lado, si realmente está preguntando si hay una forma clara de hacerlo con el UICollectionViewFlowLayout predeterminado, entonces creo que no hay forma. Incluso con las celdas de auto-tamaño de iOS8, no es sencillo. El problema fundamental, como usted dice, es que la maquinaria del diseño de flujo no proporciona ninguna forma de corregir una dimensión y permitir que otra responda. (Además, incluso si pudiera, habría una complejidad adicional en torno a la necesidad de dos pases de diseño para ajustar el tamaño de las tags de varias líneas. Esto podría no coincidir con cómo las celdas de auto-tamaño quieren calcular todos los tamaños mediante una llamada a systemLayoutSizeFittingSize).

Sin embargo, si aún desea crear un diseño tipo tabla vista con un diseño de flujo, con celdas que determinan su propio tamaño y que respondan naturalmente al ancho de la vista de colección, por supuesto que es posible. Todavía hay una manera desordenada. Lo he hecho con una “celda de dimensionamiento”, es decir, una UICollectionViewCell no mostrada que el controlador mantiene solo para calcular los tamaños de celda.

Hay dos partes en este enfoque. La primera parte es para que el delegado de la vista de recostackción calcule el tamaño de celda correcto, tomando en cuenta el ancho de la vista de colección y usando la celda de dimensionamiento para calcular la altura de la celda.

En su UICollectionViewDelegateFlowLayout, implementa un método como este:

 func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize { // NOTE: here is where we say we want cells to use the width of the collection view let requiredWidth = collectionView.bounds.size.width // NOTE: here is where we ask our sizing cell to compute what height it needs let targetSize = CGSize(width: requiredWidth, height: 0) /// NOTE: populate the sizing cell's contents so it can compute accurately self.sizingCell.label.text = items[indexPath.row] let adequateSize = self.sizingCell.preferredLayoutSizeFittingSize(targetSize) return adequateSize } 

Esto hará que la vista de recostackción establezca el ancho de la celda en función de la vista de colección adjunta, pero luego le pedirá a la celda de dimensionamiento que calcule la altura.

La segunda parte es hacer que la celda de dimensionamiento use sus propias restricciones AL para calcular la altura. Esto puede ser más difícil de lo que debería ser, debido a la forma en que las UILabel de varias líneas requieren efectivamente un proceso de diseño en dos etapas. El trabajo se realiza en el método preferredLayoutSizeFittingSize , que es así:

  /* Computes the size the cell will need to be to fit within targetSize. targetSize should be used to pass in a width. the returned size will have the same width, and the height which is calculated by Auto Layout so that the contents of the cell (ie, text in the label) can fit within that width. */ func preferredLayoutSizeFittingSize(targetSize:CGSize) -> CGSize { // save original frame and preferredMaxLayoutWidth let originalFrame = self.frame let originalPreferredMaxLayoutWidth = self.label.preferredMaxLayoutWidth // assert: targetSize.width has the required width of the cell // step1: set the cell.frame to use that width var frame = self.frame frame.size = targetSize self.frame = frame // step2: layout the cell self.setNeedsLayout() self.layoutIfNeeded() self.label.preferredMaxLayoutWidth = self.label.bounds.size.width // assert: the label's bounds and preferredMaxLayoutWidth are set to the width required by the cell's width // step3: compute how tall the cell needs to be // this causes the cell to compute the height it needs, which it does by asking the // label what height it needs to wrap within its current bounds (which we just set). let computedSize = self.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize) // assert: computedSize has the needed height for the cell // Apple: "Only consider the height for cells, because the contentView isn't anchored correctly sometimes." let newSize = CGSize(width:targetSize.width,height:computedSize.height) // restre old frame and preferredMaxLayoutWidth self.frame = originalFrame self.label.preferredMaxLayoutWidth = originalPreferredMaxLayoutWidth return newSize } 

(Este código está adaptado del código de muestra de Apple del código de muestra de la sesión WWDC2014 en “Vista de colección avanzada”).

Un par de puntos para notar. Utiliza layoutIfNeeded () para forzar el diseño de toda la celda, a fin de calcular y establecer el ancho de la etiqueta. Pero eso no es suficiente. Creo que también necesita establecer preferredMaxLayoutWidth para que la etiqueta use ese ancho con Diseño automático. Y solo entonces puede usar systemLayoutSizeFittingSize para que la celda calcule su altura teniendo en cuenta la etiqueta.

¿Me gusta este enfoque? ¡¡No!! Se siente demasiado complejo, y lo hace dos veces. Pero mientras el rendimiento no se convierta en un problema, preferiría realizar el diseño dos veces en tiempo de ejecución que tener que definirlo dos veces en el código, que parece ser la única alternativa.

Mi esperanza es que eventualmente las células auto-dimensionadas funcionen de manera diferente y todo esto será mucho más simple.

Proyecto de ejemplo que lo muestra en el trabajo.

Pero, ¿por qué no usar células auto-dimensionadas?

En teoría, las nuevas instalaciones de iOS8 para “células auto-dimensionadas” deberían hacer esto innecesario. Si ha definido una celda con Diseño automático (AL), entonces la vista de recostackción debe ser lo suficientemente inteligente como para permitir que se ajuste a sí misma y se distribuya correctamente. En la práctica, no he visto ejemplos que hayan hecho que esto funcione con tags de varias líneas. Creo que esto se debe en parte a que el mecanismo de auto-dimensionamiento de la célula todavía tiene fallas.

Pero apostaría a que es principalmente debido a la habitual dificultad de Auto Layout y las tags, que es que UILabels requiere básicamente un proceso de diseño en dos pasos. No tengo claro cómo puede realizar ambos pasos con celdas de auto-tamaño.

Y como dije, este es realmente un trabajo para un diseño diferente. Es parte de la esencia del diseño de flujo que posiciona las cosas que tienen un tamaño, en lugar de fijar un ancho y les permite elegir su altura.

Y ¿qué pasa con preferredLayoutAttributesFittingAttributes:?

El método preferredLayoutAttributesFittingAttributes: es una pista falsa, creo. Eso solo está allí para usarse con el nuevo mecanismo de celda de auto-tamaño. Entonces esta no es la respuesta, siempre que ese mecanismo no sea confiable.

¿Y qué pasa con systemlayoutSizeFittingSize 😕

Tienes razón, los documentos son confusos.

Los documentos en systemLayoutSizeFittingSize: y systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority: ambos sugieren que solo debe pasar UILayoutFittingCompressedSize y UILayoutFittingExpandedSize como targetSize . Sin embargo, la firma del método en sí, los comentarios del encabezado y el comportamiento de las funciones indican que responden al valor exacto del parámetro targetSize .

De hecho, si configura UICollectionViewFlowLayoutDelegate.estimatedItemSize , para habilitar el nuevo mecanismo de celda de auto-tamaño, ese valor parece pasar como targetSize. Y UILabel.systemLayoutSizeFittingSize parece devolver los mismos valores exactos que UILabel.sizeThatFits . Esto es sospechoso, dado que se supone que el argumento de sizeThatFits: es un objective aproximado y se supone que el argumento de sizeThatFits: es un tamaño de circunscripción máximo.

Más recursos

Si bien es triste pensar que tal requisito de rutina requiera “recursos de investigación”, creo que sí. Buenos ejemplos y discusiones son:

Hay una manera más clara de hacer esto que algunas de las otras respuestas aquí, y funciona bien. Debe ser eficaz (las vistas de colección cargan rápidamente, no hay pases de diseño automático innecesarios, etc.) y no tiene ningún ‘número mágico’ como un ancho fijo de vista de colección. Cambiar el tamaño de la vista de colección, por ejemplo, en la rotación, y luego invalidar el diseño debería funcionar también.

1. Cree la siguiente subclase de diseño de flujo

 class HorizontallyFlushCollectionViewFlowLayout: UICollectionViewFlowLayout { // Don't forget to use this class in your storyboard (or code, .xib etc) override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? { let attributes = super.layoutAttributesForItemAtIndexPath(indexPath)?.copy() as? UICollectionViewLayoutAttributes guard let collectionView = collectionView else { return attributes } attributes?.bounds.size.width = collectionView.bounds.width - sectionInset.left - sectionInset.right return attributes } override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let allAttributes = super.layoutAttributesForElementsInRect(rect) return allAttributes?.flatMap { attributes in switch attributes.representedElementCategory { case .Cell: return layoutAttributesForItemAtIndexPath(attributes.indexPath) default: return attributes } } } } 

2. Registre su vista de colección para el tamaño automático

 // The provided size should be a plausible estimate of the actual // size. You can set your item size in your storyboard // to a good estimate and use the code below. Otherwise, // you can provide it manually too, eg CGSize(width: 100, height: 100) flowLayout.estimatedItemSize = flowLayout.itemSize 

3. Use el ancho predefinido + altura personalizada en su subclase de celda

 override func preferredLayoutAttributesFittingAttributes(layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { layoutAttributes.bounds.size.height = systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height return layoutAttributes } 

Una forma sencilla de hacerlo en iOS 9 en unas pocas líneas de códigos: el ejemplo de manera horizontal (fijando su altura a su altura de vista de colección):

Inicie su distribución de flujo de vista de colección con un tamaño de tamaño estimatedItemSize para habilitar la celda de auto-tamaño:

 self.scrollDirection = UICollectionViewScrollDirectionHorizontal; self.estimatedItemSize = CGSizeMake(1, 1); 

Implemente el delegado de diseño de vista de colección (en su controlador de vista la mayoría de las veces), collectionView:layout:sizeForItemAtIndexPath: El objective aquí es establecer la altura (o ancho) fija en la dimensión Vista de Colección. El valor 10 puede ser cualquier cosa, pero debe establecerlo en un valor que no rompa las restricciones:

 - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { return CGSizeMake(10, CGRectGetHeight(collectionView.bounds)); } 

Anule su método preferredLayoutAttributesFittingAttributes: celda preferredLayoutAttributesFittingAttributes: esta parte realmente calcula el ancho de su celda dinámica en función de sus restricciones de diseño automático y la altura que acaba de establecer:

 - (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes { UICollectionViewLayoutAttributes *attributes = [layoutAttributes copy]; float desiredWidth = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].width; CGRect frame = attributes.frame; frame.size.width = desiredWidth; attributes.frame = frame; return attributes; } 

Intente corregir su ancho en los atributos de diseño preferidos:

 - (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes { UICollectionViewLayoutAttributes *attributes = [[super preferredLayoutAttributesFittingAttributes:layoutAttributes] copy]; CGSize newSize = [self systemLayoutSizeFittingSize:CGSizeMake(FIXED_WIDTH,layoutAttributes.size) withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel]; CGRect newFrame = attr.frame; newFrame.size.height = size.height; attr.frame = newFrame; return attr; } 

Naturalmente, también debe asegurarse de configurar correctamente su diseño para:

 UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout *) self.collectionView.collectionViewLayout; flowLayout.estimatedItemSize = CGSizeMake(FIXED_WIDTH, estimatedHeight)]; 

Aquí hay algo que pongo en Github que usa celdas de ancho constante y admite el tipo dynamic para que la altura de las celdas se actualice a medida que cambia el tamaño de la fuente del sistema.

SÍ se puede hacer utilizando el diseño automático mediante progtwigción y estableciendo restricciones en el guión gráfico o xib. Debe agregar restricción para que el tamaño del ancho permanezca constante y establecer una altura mayor o igual que.
http://www.thinkandbuild.it/learn-to-love-auto-layout-programmatically/

http://www.cocoanetics.com/2013/08/variable-sized-items-in-uicollectionview/
Espero que esto sea útil y solucione su problema.