UICollectionView Auto Dimensionamiento de celdas con layout automático

Estoy tratando de get tamaño auto UICollectionViewCells trabajando con Auto Layout, pero parece que no puedo lograr que las celdas se adapten al contenido. Estoy teniendo problemas para entender cómo se actualiza el tamaño de la celda a partir del contenido de lo que está dentro del contenido de la celda.

Aquí está la configuration que he probado:

  • UICollectionViewCell personalizado con una UITextView en su ContentView.
  • El desplazamiento para UITextView está deshabilitado.
  • La restricción horizontal de ContentView es: "H: | [_textView (320)]", es decir, UITextView está fijado a la izquierda de la celda con un ancho explícito de 320.
  • La restricción vertical de ContentView es: "V: | -0 – [_ textView]", es decir, UITextView anclado en la parte superior de la celda.
  • UITextView tiene una restricción de altura establecida en una constante que los informes de UITextView encajarán en el text.

Esto es lo que se ve con el background de celda en rojo y el background de UITextView configurado en Azul: fondo de celda rojo, fondo de UITextView azul

Puse el proyecto con el que he estado jugando en GitHub aquí .

Actualizado para iOS 9

Después de ver mi ruptura de la solución de GitHub bajo iOS 9, finalmente obtuve el time para investigar el problema por completo. Ahora he actualizado el repository para include varios ejemplos de configuraciones diferentes para celdas de tamaño propio. Mi conclusión es que las células de tamaño propio son grandiosas en teoría pero desorderadas en la práctica. Una palabra de precaución al proceder con las células auto dimensionadas.

TL; DR

Mira mi proyecto GitHub


Las celdas de tamaño propio solo son compatibles con el layout del flujo, así que asegúrese de que eso es lo que está utilizando.

Hay dos cosas que necesita configurar para que las celdas de tamaño automático funcionen.

1. Establezca estimatedItemSize en UICollectionViewFlowLayout

El layout del flujo se volverá dynamic en la naturaleza una vez que establezca la propiedad estimatedItemSize .

 self.flowLayout.estimatedItemSize = CGSize(width: 100, height: 100) 

2. Agregue soporte para el tamaño en su subclass celular

Esto viene en 2 sabores; Auto-Layout o anulación personalizada de prefernetworkingLayoutAttributesFittingAttributes .

Crear y configurar celdas con layout automático

No voy a entrar en detalles sobre esto ya que hay una publicación SO shiny sobre la configuration de restricciones para una célula. Solo tenga cuidado de que Xcode 6 rompió un montón de cosas con iOS 7, así que si usted admite iOS 7, necesitará hacer cosas como asegurarse de que el autoresizingMask está configurado en el contenido de la celda y que los límites de contentView se establecen como los límites de la celda cuando la celda está cargada (es decir, awakeFromNib ).

Las cosas que debe tener en count es que su celda debe estar más seriamente limitada que una celda de vista de tabla. Por ejemplo, si desea que su ancho sea dynamic, su celda necesita una restricción de altura. Del mismo modo, si desea que la altura sea dinámica, necesitará una restricción de ancho para su celda.

Implementar prefernetworkingLayoutAttributesFittingAttributes en su celda personalizada

Cuando se llama a esta function, su vista ya se configuró con contenido (es decir, se ha llamado a cellForItem ). Suponiendo que sus restricciones se hayan configurado adecuadamente, podría tener una implementación como esta:

 //forces the system to do one layout pass var isHeightCalculated: Bool = false override func prefernetworkingLayoutAttributesFittingAttributes(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { //Exhibit A - We need to cache our calculation to prevent a crash. if !isHeightCalculated { setNeedsLayout() layoutIfNeeded() let size = contentView.systemLayoutSizeFittingSize(layoutAttributes.size) var newFrame = layoutAttributes.frame newFrame.size.width = CGFloat(ceilf(Float(size.width))) layoutAttributes.frame = newFrame isHeightCalculated = true } return layoutAttributes } 

NOTA En iOS 9, el comportamiento cambió un poco que podría causar fallas en su implementación si no tiene cuidado (Vea más aquí ). Cuando implementa prefernetworkingLayoutAttributesFittingAttributes debes asegurarte de que solo cambias el marco de tus attributes de layout una vez. Si no hace esto, el layout llamará a su implementación de forma indefinida y eventualmente se bloqueará. Una solución es almacenar en caching el tamaño calculado en su celda e invalidar esto cada vez que reutilice la celda o cambie su contenido como lo hice con la propiedad isHeightCalculated .

Experimente su layout

En este punto, deberías tener celdas dinámicas "funcionales" en tu colecciónView. Todavía no he encontrado la solución list para usar lo suficiente durante mis testings, así que siéntete libre de comentar si lo has hecho. Sigue sintiendo que UITableView gana la batalla por el dimensionamiento dynamic en mi humilde opinión.

Advertencias

Tenga muy en count que si está utilizando celdas prototipo para calcular el estimadoItemSize, esto se romperá si su XIB usa classs de tamaño . La razón de esto es que cuando cargues tu celular de un XIB, su class de tamaño se configurará con Undefined . Esto solo se romperá en iOS 8 y más ya que en iOS 7 la class de tamaño se cargará en function del dispositivo (iPad = Regular-Any, iPhone = Compact-Any). Puede establecer el estimadoItemSize sin cargar el XIB, o puede cargar la celda desde el XIB, agregarlo a la colecciónView (esto establecerá el traitCollection), realizar el layout y luego eliminarlo de la vista de supervisión. Alternativamente, también puede hacer que su célula anule el traitCollection getter de traitCollection y devuelva los rasgos apropiados. Tu decides.

Déjame saber si me perdí algo, espero haber ayudado y encoding de buena suerte.


Unos pocos cambios key en la respuesta de Daniel Galasso arreglaron todos mis problemas. Lamentablemente, no tengo suficiente reputación para comentar directamente (aún).

En el paso 1, al usar el layout automático, simplemente agregue una UIView padre único a la celda. TODO dentro de la celda debe ser una subvista del padre. Eso respondió a todos mis problemas. Mientras XCode agrega esto para UITableViewCells automáticamente, no lo hace (pero debería hacerlo) para UICollectionViewCells. De acuerdo con los documentos :

Para configurar el aspecto de su celda, agregue las vistas necesarias para presentar el contenido del elemento de datos como subvenciones a la vista en la propiedad contentView. No agregue directamente subvenciones directamente a la celda.

Luego omita el paso 3 por completo. No es necesario

En iOS10 hay una nueva constante llamada UICollectionViewFlowLayoutAutomaticSize , por lo que en su lugar:

 self.flowLayout.estimatedItemSize = CGSize(width: 100, height: 100) 

puedes usar esto:

 self.flowLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize 

Tiene un mejor performance, especialmente cuando las celdas en la vista de colección tienen wid constantes

En iOS 10 este es un process muy simple de 2 pasos.

  1. Asegúrese de que todos los contenidos de su celda estén ubicados dentro de una única UIView (o dentro de un descendiente de UIView como UIStackView, lo que simplifica mucho la reproducción automática). Al igual que con cambiar el tamaño dinámicamente de UITableViewCells, la jerarquía de vista completa debe tener restricciones configuradas desde el contenedor más externo hasta la vista más interna. Eso incluye restricciones entre UICollectionViewCell y childview inmediato

  2. Indique el flujo de salida de su UICollectionView a tamaño automáticamente

     yourFlowLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize 

Después de luchar con esto durante algún time, noté que el cambio de tamaño no funciona para UITextViews si no deshabilita el desplazamiento:

 let textView = UITextView() textView.scrollEnabled = false 

Hice una vista de colección de altura de celda dinámica. Aquí está git hub repo .

Y, explique por qué se llama a PrefernetworkingLayoutAttributesFittingAttributes más de una vez. En realidad, se llamará al less 3 veces.

La image del logging de la console: introduzca la descripción de la imagen aquí

1st prefernetworkingLayoutAttributesFittingAttributes :

 (lldb) po layoutAttributes <UICollectionViewLayoutAttributes: 0x7fa405c290e0> index path: (<NSIndexPath: 0xc000000000000016> {length = 2, path = 0 - 0}); frame = (15 12; 384 57.5); (lldb) po self.collectionView <UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 0); 

El layoutAttributes.frame.size.height es el estado actual 57.5 .

2nd prefernetworkingLayoutAttributesFittingAttributes :

 (lldb) po layoutAttributes <UICollectionViewLayoutAttributes: 0x7fa405c16370> index path: (<NSIndexPath: 0xc000000000000016> {length = 2, path = 0 - 0}); frame = (15 12; 384 534.5); (lldb) po self.collectionView <UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 0); 

La altura del marco de la celda cambió a 534.5 como esperábamos. Pero, la vista de la colección sigue siendo la altura cero.

3rd prefernetworkingLayoutAttributesFittingAttributes :

 (lldb) po layoutAttributes <UICollectionViewLayoutAttributes: 0x7fa403d516a0> index path: (<NSIndexPath: 0xc000000000000016> {length = 2, path = 0 - 0}); frame = (15 12; 384 534.5); (lldb) po self.collectionView <UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 477); 

Puede ver que la altura de la vista de la colección se cambió de 0 a 477 .

El comportamiento es similar a manejar desplazamiento:

 1. Before self-sizing cell 2. Validated self-sizing cell again after other cells recalculated. 3. Did changed self-sizing cell 

Al principio, pensé que este método solo llamaba una vez. Entonces codifiqué como el siguiente:

 CGRect frame = layoutAttributes.frame; frame.size.height = frame.size.height + self.collectionView.contentSize.height; UICollectionViewLayoutAttributes* newAttributes = [layoutAttributes copy]; newAttributes.frame = frame; return newAttributes; 

Esta línea:

 frame.size.height = frame.size.height + self.collectionView.contentSize.height; 

causará que el sistema llame a un bucle infinito y se bloquee la aplicación.

Cualquier tamaño cambiado, validará todas las celdas 'prefernetworkingLayoutAttributesFittingAttributes una y otra vez hasta que las posiciones de todas las celdas (es decir, los cuadros) no cambien más.

El método de ejemplo anterior no comstack. Aquí hay una versión corregida (pero sin probar si funciona o no).

 override func prefernetworkingLayoutAttributesFittingAttributes(layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { let attr: UICollectionViewLayoutAttributes = layoutAttributes.copy() as! UICollectionViewLayoutAttributes var newFrame = attr.frame self.frame = newFrame self.setNeedsLayout() self.layoutIfNeeded() let desinetworkingHeight: CGFloat = self.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height newFrame.size.height = desinetworkingHeight attr.frame = newFrame return attr } 

A quien sea que pueda ayudar,

Tuve ese desagradable error si estimatedItemSize se configuró. Incluso si numberOfItemsInSection 0 en numberOfItemsInSection . Por lo tanto, las propias células y su layout automático no fueron la causa del locking … La colecciónView simplemente se bloqueó, incluso cuando estaba vacía, simplemente porque estimatedItemSize se configuró para autoimage.

En mi caso reorganicé mi proyecto, desde un controller que contenía una colecciónView a un collectionViewController, y funcionó.

Imagínate.

Si implementa el método UICollectionViewDelegateFlowLayout:

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

Cuando llame a collectionview performBatchUpdates:completion: la altura de tamaño usará sizeForItemAtIndexPath lugar de prefernetworkingLayoutAttributesFittingAttributes .

El process de representación de performBatchUpdates:completion el método prefernetworkingLayoutAttributesFittingAttributes pero ignora sus cambios.

Actualiza más información:

  • Si usa flowLayout.estimatedItemSize , sugiera usar la versión posterior de iOS8.3. Antes de iOS8.3, se bloqueará [super layoutAttributesForElementsInRect:rect]; . El post de error es

    *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil'

  • Segundo, en la versión iOS8.x, flowLayout.estimatedItemSize hará que la configuration de inserción de sección diferente no funcione. es decir, function: (UIEdgeInsets)collectionView:layout:insetForSectionAtIndex:

Para cualquiera que probó todo sin suerte, esto es lo único que consiguió que funcione para mí. Para las tags multilínea dentro de la celda, intente agregar esta línea mágica:

 label.prefernetworkingMaxLayoutWidth = 200 

Más información: aquí

¡Aclamaciones!

Intenté usar estimatedItemSize pero hubo un montón de errores al insert y eliminar celdas si estimatedItemSize no era exactamente igual a la altura de la celda. Dejé de configurar estimatedItemSize e implementé celdas dinámicas utilizando una celda prototipo. así es como se hace:

cree este protocolo:

 protocol SizeableCollectionViewCell { func fittedSize(forConstrainedSize size: CGSize)->CGSize } 

implementa este protocolo en tu UICollectionViewCell personalizado:

 class YourCustomCollectionViewCell: UICollectionViewCell, SizeableCollectionViewCell { @IBOutlet private var mTitle: UILabel! @IBOutlet private var mDescription: UILabel! @IBOutlet private var mContentView: UIView! @IBOutlet private var mTitleTopConstraint: NSLayoutConstraint! @IBOutlet private var mDesciptionBottomConstraint: NSLayoutConstraint! func fittedSize(forConstrainedSize size: CGSize)->CGSize { let fittedSize: CGSize! //if height is greatest value, then it's dynamic, so it must be calculated if size.height == CGFLoat.greatestFiniteMagnitude { var height: CGFloat = 0 /*now here's where you want to add all the heights up of your views. apple provides a method called sizeThatFits(size:), but it's not implemented by default; except for some concrete subclasses such as UILabel, UIButton, etc. search to see if the classes you use implement it. here's how it would be used: */ height += mTitle.sizeThatFits(size).height height += mDescription.sizeThatFits(size).height height += mCustomView.sizeThatFits(size).height //you'll have to implement this in your custom view //anything that takes up height in the cell has to be included, including top/bottom margin constraints height += mTitleTopConstraint.constant height += mDescriptionBottomConstraint.constant fittedSize = CGSize(width: size.width, height: height) } //else width is greatest value, if not, you did something wrong else { //do the same thing that's done for height but with width, remember to include leading/trailing margins in calculations } return fittedSize } } 

ahora haga que su controller se ajuste a UICollectionViewDelegateFlowLayout , y en él, tenga este campo:

 class YourViewController: UIViewController, UICollectionViewDelegateFlowLayout { private var mCustomCellPrototype = UINib(nibName: <name of the nib file for your custom collectionviewcell>, bundle: nil).instantiate(withOwner: nil, options: nil).first as! SizeableCollectionViewCell } 

se usará como una celda prototipo para enlazar datos y luego determinar cómo esos datos afectaron la dimensión que desea que sea dinámica

finalmente, la collectionView(:layout:sizeForItemAt:) UICollectionViewDelegateFlowLayout's collectionView(:layout:sizeForItemAt:) tiene que ser implementada:

 class YourViewController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { private var mDataSource: [CustomModel] func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath)->CGSize { //bind the prototype cell with the data that corresponds to this index path mCustomCellPrototype.bind(model: mDataSource[indexPath.row]) //this is the same method you would use to reconfigure the cells that you dequeue in collectionView(:cellForItemAt:). i'm calling it bind //define the dimension you want constrained let width = UIScreen.main.bounds.size.width - 20 //the width you want your cells to be let height = CGFloat.greatestFiniteMagnitude //height has the greatest finite magnitude, so in this code, that means it will be dynamic let constrainedSize = CGSize(width: width, height: height) //determine the size the cell will be given this data and return it return mCustomCellPrototype.fittedSize(forConstrainedSize: constrainedSize) } } 

y eso es. Devolviendo el tamaño de la celda en collectionView(:layout:sizeForItemAt:) de esta manera evitando que tenga que usar estimatedItemSize , e insertando y eliminando celdas funciona perfectamente.

  1. Agregue flowLayout en viewDidLoad ()

     override func viewDidLoad() { super.viewDidLoad() if let flowLayout = infoCollection.collectionViewLayout as? UICollectionViewFlowLayout { flowLayout.estimatedItemSize = CGSize(width: 1, height:1) } } 
  2. Además, configure una UIView como mainContainer para su celda y añada todas las vistas necesarias dentro de ella.

  3. Consulte este tutorial impresionante y alucinante para get más references: UICollectionView con celda de autocaptura utilizando autolayout en iOS 9 y 10