UICollectionView Self Sizing Cells con diseño automático

Estoy intentando que UICollectionViewCells tamaño con el diseño automático, pero parece que no logro que las celdas tengan el tamaño UICollectionViewCells para el contenido. Tengo problemas para entender cómo se actualiza el tamaño de la celda a partir del contenido de lo que está dentro de contentView de la celda.

Aquí está la configuración que he intentado:

  • UICollectionViewCell personalizado con una UITextView en su contentView.
  • Desplazarse para el UITextView está deshabilitado.
  • La restricción horizontal de contentView es: “H: | [_textView (320)]”, es decir, el UITextView está UITextView 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, el UITextView fijado en la parte superior de la celda.
  • El UITextView tiene una restricción de altura establecida en una constante que los informes de UITextView se ajustarán al texto.

Esto es lo que parece con el fondo de la celda configurado en rojo, y el fondo UITextView configurado en Azul: fondo de la celda rojo, fondo UITextView azul

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

Actualizado para Swift 4

systemLayoutSizeFittingSize renombrado a systemLayoutSizeFitting


Actualizado para iOS 9

Después de ver mi solución de GitHub romperse en iOS 9, finalmente tuve tiempo de investigar el problema por completo. Ahora he actualizado el repository para incluir varios ejemplos de diferentes configuraciones para celdas de auto-dimensionamiento. Mi conclusión es que las celdas de tamaño propio son geniales en teoría pero desordenadas en la práctica. Una palabra de advertencia al proceder con las celdas de auto-dimensionamiento.

TL; DR

Mira mi proyecto de GitHub


Las celdas de auto-dimensionamiento solo son compatibles con el diseño de flujo, así que asegúrese de que sea lo que está usando.

Hay dos cosas que necesita configurar para que las celdas de auto-tamaño funcionen.

1. Establezca estimatedItemSize en UICollectionViewFlowLayout

El diseño de flujo se volverá dynamic en naturaleza una vez que establezca la propiedad de estimatedItemSize tamaño.

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

2. Agregue soporte para el dimensionamiento en su subclase de celda

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

Crear y configurar celdas con diseño automático

No voy a entrar en detalles sobre esto ya que hay una shiny publicación SO sobre la configuración de restricciones para una celda. Solo tenga cuidado de que Xcode 6 haya roto un montón de cosas con iOS 7, de modo que, si es compatible con iOS 7, deberá hacer cosas como asegurar que el autoresizingMask esté configurado en contentView de la celda y que los límites de contentView estén establecidos como límites de la celda cuando la celda está cargada (es decir, awakeFromNib ).

Las cosas que debe tener en cuenta es que su celda debe estar más restringida que una celda de vista de tabla. Por ejemplo, si quiere 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.

Implemente preferredLayoutAttributesFittingAttributes en su celda personalizada

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

 //forces the system to do one layout pass var isHeightCalculated: Bool = false override func preferredLayoutAttributesFittingAttributes(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { //Exhibit A - We need to cache our calculation to prevent a crash. if !isHeightCalculated { setNeedsLayout() layoutIfNeeded() let size = contentView.systemLayoutSizeFitting(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 y podría causar lockings en su implementación si no tiene cuidado (vea más aquí ). Cuando implemente preferredLayoutAttributesFittingAttributes , debe asegurarse de que solo cambie el marco de sus atributos de diseño una vez. Si no lo hace, el diseño llamará a su implementación de forma indefinida y eventualmente se bloqueará. Una solución es almacenar en caché el tamaño calculado en su celda e invalidarlo cada vez que reutilice la celda o cambie su contenido como lo hice con la propiedad isHeightCalculated .

Experimenta tu diseño

En este punto, debe tener celdas dinámicas “funcionando” en su collectionView. Todavía no he encontrado suficiente la solución lista para usar durante mis pruebas, así que no dude en hacer un comentario si lo tiene. Todavía se siente como UITableView gana la batalla por el tamaño dynamic en mi humilde opinión.

Advertencias

Tenga en cuenta que si usa células prototipo para calcular el tamaño del tamaño estimado, esto se romperá si su XIB usa clases de tamaño . La razón de esto es que cuando carga su celda desde un XIB, su clase de tamaño se configurará con No Undefined . Esto solo se romperá en iOS 8 y posteriores, ya que en iOS 7 la clase de tamaño se cargará según el dispositivo (iPad = Regular-Any, iPhone = Compact-Any). Puede establecer el tamaño de tamaño estimado sin cargar el XIB, o puede cargar la celda desde el XIB, agregarlo a collectionView (esto configurará el traitCollection), realizar el diseño y luego eliminarlo de la supervista. Alternativamente, también puede hacer que su celda anule el captador de traitCollection y devuelva los rasgos apropiados. Tu decides.

Avíseme si me perdí algo, espero haber ayudado y la buena suerte de la encoding


Algunos cambios clave en la respuesta de Daniel Galasko solucionaron todos mis problemas. Desafortunadamente, no tengo suficiente reputación para comentar directamente (todavía).

En el paso 1, cuando usa el diseño automático, simplemente agregue un único UIView padre a la celda. TODO dentro de la celda debe ser una subvista del padre. Eso respondió a todos mis problemas. Mientras que XCode agrega esto para UITableViewCells automáticamente, no lo hace (pero debería) 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 subvistas a la vista en la propiedad contentView. No agregue directamente subvistas a la celda en sí.

Entonces omita el paso 3 por completo. No es necesario

En iOS10 hay una nueva constante llamada UICollectionViewFlowLayoutAutomaticSize , así que en su lugar:

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

puedes usar esto:

 self.flowLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize 

Tiene un mejor rendimiento, especialmente cuando las celdas en su vista de colección tienen constante wid

En iOS 10+, este es un proceso muy simple de 2 pasos.

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

  2. Indique el diagtwig de flujo de su UICollectionView para el tamaño automáticamente

     yourFlowLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize 
  1. Agregar 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 un UIView como mainContainer para su celda y agregue todas las vistas requeridas dentro de él.

  3. Refiérase a este impresionante y alucinante tutorial para mayor referencia: UICollectionView con autosizing cell usando el autolayout en iOS 9 y 10

Después de luchar con esto por un tiempo, noté que el cambio de tamaño no funciona para UITextViews si no deshabilita el desplazamiento:

 let textView = UITextView() textView.scrollEnabled = false 

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

Y descubra por qué preferredLayoutAttributesFittingAttributes se llama más de una vez. En realidad, se llamará al menos 3 veces.

La imagen de registro de la consola: enter image description here

1st preferredLayoutAttributesFittingAttributes :

 (lldb) po layoutAttributes  index path: ( {length = 2, path = 0 - 0}); frame = (15 12; 384 57.5); (lldb) po self.collectionView  

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

2nd preferredLayoutAttributesFittingAttributes :

 (lldb) po layoutAttributes  index path: ( {length = 2, path = 0 - 0}); frame = (15 12; 384 534.5); (lldb) po self.collectionView  

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

3rd preferredLayoutAttributesFittingAttributes :

 (lldb) po layoutAttributes  index path: ( {length = 2, path = 0 - 0}); frame = (15 12; 384 534.5); (lldb) po self.collectionView  

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

El comportamiento es similar al desplazamiento de 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. Así que 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; 

hará que el sistema invoque un bucle infinito y se bloquee la aplicación.

Cualquier tamaño cambiado, validará todas las celdas 'preferredLayoutAttributesFittingAttributes una y otra vez hasta que las posiciones de cada celda (es decir, marcos) ya no cambien.

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

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

A quien pueda ayudar,

Tuve esa desagradable caída si se configuró EstimateItemSize. Incluso si devolviera 0 en numberOfItemsInSection . Por lo tanto, las celdas en sí mismas y su diseño automático no fueron la causa de la falla … La colecciónView simplemente colapsó, incluso cuando estaba vacía, simplemente porque se estiró el tamaño del tamaño estimatedItemSize para el auto-tamaño.

En mi caso reorganicé mi proyecto, desde un controlador que contenía un collectionView 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 del tamaño usará sizeForItemAtIndexPath lugar de preferredLayoutAttributesFittingAttributes .

El proceso de representación de performBatchUpdates:completion pasará por el método preferredLayoutAttributesFittingAttributes pero ignorará los cambios.

Actualiza más información:

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

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

  • En segundo lugar, en la versión iOS8.x, flowLayout.estimatedItemSize provocará que la configuración de inserción de sección diferente no funcione. es decir, función: (UIEdgeInsets)collectionView:layout:insetForSectionAtIndex:

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

 label.preferredMaxLayoutWidth = 200 

Más información: aquí

¡Aclamaciones!

Intenté usar estimatedItemSize pero hubo un montón de errores al insertar y eliminar celdas si el estimatedItemSize no era exactamente igual a la altura de la celda. Dejé de establecer estimatedItemSize e implementé las células dinámicas mediante el uso de una celda prototipo. así es como se hace eso:

crea este protocolo:

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

implemente este protocolo en su 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 controlador se ajuste a UICollectionViewDelegateFlowLayout , y en él, tenga este campo:

 class YourViewController: UIViewController, UICollectionViewDelegateFlowLayout { private var mCustomCellPrototype = UINib(nibName: , bundle: nil).instantiate(withOwner: nil, options: nil).first as! SizeableCollectionViewCell } 

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

finalmente, se debe UICollectionViewDelegateFlowLayout's collectionView(:layout:sizeForItemAt:) :

 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. Devolver el tamaño de la celda en collectionView(:layout:sizeForItemAt:) de esta manera me impide tener que utilizar estimatedItemSize , y la inserción y eliminación de células funciona a la perfección.