UICollectionView dentro de un UITableViewCell - ¿altura dinámica?


125

Una de nuestras pantallas de aplicación requiere que coloquemos un UICollectionViewarchivo UITableViewCell. Esto UICollectionViewtendrá un número dinámico de elementos, lo que dará como resultado una altura que también debe calcularse dinámicamente. Sin embargo, tengo problemas al intentar calcular la altura del archivo UICollectionView.

Nuestro UIViewControlleresquema general se creó en Storyboards y hace uso del diseño automático. Pero, no sé cómo aumentar dinámicamente la altura de la en UITableViewCellfunción de la altura de UICollectionView.

¿Alguien puede dar algunos consejos o consejos sobre cómo lograr esto?


¿Puede contarnos un poco más sobre el diseño que está intentando realizar? ¿Es la altura del UITableViewCelltablero diferente de su vista de mesa real? ¿Necesita desplazamiento vertical tanto en el UICollectionViewcomo en elUITableView
apouche

3
Se supone que la altura de UITableViewCell es dinámica, basada en la altura de UICollectionView que forma parte de la celda de vista de tabla. El diseño de mi UICollectionView es de 4 columnas y un número dinámico de filas. Entonces, con 100 elementos, tendría 4 columnas y 25 filas en mi UICollectionView. La altura de la celda en particular, como se indica en heightForRowAtIndexPath de UITableView, debe poder ajustarse en función del tamaño de esa UICollectionView. ¿Está un poco más claro?
Shadowman

@Shadowman, por favor sugiera una solución para esta situación
Ravi Ojha

Respuestas:


127

La respuesta correcta es , PUEDE hacer esto.

Me encontré con este problema hace algunas semanas. En realidad, es más fácil de lo que piensas. Coloque sus celdas en NIB (o guiones gráficos) y fíjelas para permitir que el diseño automático haga todo el trabajo

Dada la siguiente estructura:

TableView

TableViewCell

CollectionView

ColecciónViewCell

ColecciónViewCell

ColecciónViewCell

[... número variable de celdas o diferentes tamaños de celda]

La solución es decirle al diseño automático que calcule primero los tamaños de collectionViewCell, luego la vista de colección contentSize, y utilícela como el tamaño de su celda. Este es el método UIView que "hace la magia":

-(void)systemLayoutSizeFittingSize:(CGSize)targetSize 
     withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority 
           verticalFittingPriority:(UILayoutPriority)verticalFittingPriority

Debe establecer aquí el tamaño de TableViewCell, que en su caso es contentSize de CollectionView.

ColecciónViewCell

En CollectionViewCell , debe indicarle a la celda que se diseñe cada vez que cambie el modelo (por ejemplo: establece un UILabel con un texto, luego la celda tiene que ser diseñada nuevamente).

- (void)bindWithModel:(id)model {
    // Do whatever you may need to bind with your data and 
    // tell the collection view cell's contentView to resize
    [self.contentView setNeedsLayout];
}
// Other stuff here...

TableViewCell

El TableViewCell hace la magia. Tiene una salida a su collectionView, habilita el diseño automático para las celdas de collectionView utilizando estimadoItemSize de UICollectionViewFlowLayout .

Entonces, el truco consiste en establecer el tamaño de la celda tableView en el método systemLayoutSizeFittingSize ... (NOTA: iOS8 o posterior)

NOTA: Traté de usar el método de altura de la celda delegada de tableView. -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPathPero es demasiado tarde para que el sistema de diseño automático calcule el tamaño del contenido de CollectionView y, a veces, puede encontrar celdas redimensionadas incorrectas.

@implementation TableCell

- (void)awakeFromNib {
    [super awakeFromNib];
    UICollectionViewFlowLayout *flow = (UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout;

    // Configure the collectionView
    flow.minimumInteritemSpacing = ...;

    // This enables the magic of auto layout. 
    // Setting estimatedItemSize different to CGSizeZero 
    // on flow Layout enables auto layout for collectionView cells.
    // https://developer.apple.com/videos/play/wwdc2014-226/
    flow.estimatedItemSize = CGSizeMake(1, 1);

    // Disable the scroll on your collection view
    // to avoid running into multiple scroll issues.
    [self.collectionView setScrollEnabled:NO];
}

- (void)bindWithModel:(id)model {
    // Do your stuff here to configure the tableViewCell
    // Tell the cell to redraw its contentView        
    [self.contentView layoutIfNeeded];
}

// THIS IS THE MOST IMPORTANT METHOD
//
// This method tells the auto layout
// You cannot calculate the collectionView content size in any other place, 
// because you run into race condition issues.
// NOTE: Works for iOS 8 or later
- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority verticalFittingPriority:(UILayoutPriority)verticalFittingPriority {

    // With autolayout enabled on collection view's cells we need to force a collection view relayout with the shown size (width)
    self.collectionView.frame = CGRectMake(0, 0, targetSize.width, MAXFLOAT);
    [self.collectionView layoutIfNeeded];

    // If the cell's size has to be exactly the content 
    // Size of the collection View, just return the
    // collectionViewLayout's collectionViewContentSize.

    return [self.collectionView.collectionViewLayout collectionViewContentSize];
}

// Other stuff here...

@end

TableViewController

Recuerde habilitar el sistema de diseño automático para las celdas tableView en su TableViewController :

- (void)viewDidLoad {
    [super viewDidLoad];

    // Enable automatic row auto layout calculations        
    self.tableView.rowHeight = UITableViewAutomaticDimension;
    // Set the estimatedRowHeight to a non-0 value to enable auto layout.
    self.tableView.estimatedRowHeight = 10;

}

CRÉDITO: @rbarbera ayudó a solucionar esto


4
Si. Esto funciona. Esta debería ser la respuesta aceptada.
csotiriou

1
¿Alguien puede proporcionarme un código de muestra? Quiero mostrar varias imágenes en la vista de colección vertical, que está dentro de la celda de vista de tabla ..
Ravi Ojha

4
Finalmente, esto funcionó para mí, pero tuve que hacer otro truco: cell.bounds = CGRectMake(0, 0, CGRectGetWidth(tableView.bounds), 10000);esto "simulará" una celda larga con una vista de colección larga, por lo que la vista de colección calculará el tamaño de todas sus celdas. De lo contrario, collectionView calculó solo los tamaños de celda visibles y estableció incorrectamente la altura de talbeViewCell.
ingaham

20
cuando establezco la altura del marco al máximo systemLayoutSizeFittingSize, tengo un problema rápido al recargar la celda, las aplicaciones parecen atascarse después de self.collectionView.layoutIfNeeded(). Lo arreglé estableciendo la altura del marco con 1 en lugar de la altura máxima. Espero que sea de ayuda si alguien tropieza con este problema.
titán

6
Esto es lo que me solucionó: en systemLayoutSizeFitting, invierta el orden de lo que se muestra en la respuesta actualmente, de modo que primero se collectionView.layoutIfNeeded()ejecute, luegocollectionView.frame = CGRect.init(origin: .zero, size: CGSize.init(width: targetSize.width, height: 1))
xaphod

101

Creo que mi solución es mucho más simple que la propuesta por @PabloRomeu.

Paso 1. Cree una salida desde UICollectionViewhasta UITableViewCell subclass, donde UICollectionViewse coloca. Deja, su nombre serácollectionView

Paso 2. Agregue IB para la UICollectionViewrestricción de altura y cree una salida UITableViewCell subclasstambién. Vamos, su nombre será collectionViewHeight.

Paso 3. En tableView:cellForRowAtIndexPath:agregar código:

// deque a cell
cell.frame = tableView.bounds;
[cell layoutIfNeeded];
[cell.collectionView reloadData];
cell.collectionViewHeight.constant = cell.collectionView.collectionViewLayout.collectionViewContentSize.height;

1
¿De verdad has probado esto? No parece posible establecer una restricción de altura en la vista de colección y fijarla a los márgenes de la celda de la vista de tabla. O termina con muy pocas restricciones (por lo tanto, una advertencia de tamaño cero), o termina con restricciones en conflicto (una de las cuales se ignora). Explique cómo establecer las restricciones para esta solución.
Dale

2
Lo tengo funcionado. Agrego restricciones para collectionViewtodos los lados de UITableViewCelly establezco tableView.estimatedRowHeight = 44;ytableView.rowHeight = UITableViewAutomaticDimension;
Igor

3
Este funciona como un campeón ... simple y eficiente ... gracias. Si para alguien no funciona, probablemente la razón podría ser que debemos vincular la parte inferior de la vista de colección a la parte inferior de la celda de la vista de tabla. Entonces empezará a funcionar. Codificación feliz .. :)
Sebin Roy

2
¿Funcionaría esto cuando se cambia el tamaño de la pantalla, por ejemplo, girando un iPad? Eso cambiaría potencialmente la altura de la vista de la colección y, por lo tanto, la altura de la celda, pero si la vista de tabla no necesariamente recarga la celda en la pantalla, nunca se llamará al método cellForRowAtIndexPath: y no se le dará la oportunidad de cambiar la constante de restricción. Sin embargo, puede ser posible forzar un reloadData.
Carl Lindberg

2
¿Has probado diferentes tamaños de celda de collectionView? Ese era mi problema. Si tiene diferentes tamaños de celda de collectionView, no funcionó ... :(
Pablo Romeu

22

Tanto las vistas de tabla como las de colección son UIScrollViewsubclases y, por lo tanto, no les gusta estar incrustadas dentro de otra vista de desplazamiento, ya que intentan calcular el tamaño del contenido, reutilizar celdas, etc.

Te recomiendo que uses solo una vista de colección para todos tus propósitos.

Puede dividirlo en secciones y "tratar" el diseño de algunas secciones como una vista de tabla y otras como una vista de colección. Después de todo, no hay nada que no pueda lograr con una vista de colección que pueda con una vista de tabla.

Si tiene un diseño de cuadrícula básico para las "partes" de la vista de colección, también puede usar celdas normales de la tabla para manejarlas. Aún así, si no necesita la compatibilidad con iOS 5, debería usar mejor las vistas de colección.


1
Ha puesto "tratamiento" entre comillas, porque NO puede definir diferentes diseños por sección CollectionView, ¿verdad? No he encontrado una opción para definir diferentes diseños para diferentes secciones de vista de colección. ¿Estoy pasando por alto algo o estás hablando de responder de manera diferente a 'layoutAttributesForItemAtIndexPath' para diferentes secciones en mi propia subclase personalizada de UICollectionViewLayout?
alex da franca

Sí -> ¿Estás hablando de responder de manera diferente layoutAttributesForItemAtIndexPathpara diferentes secciones en mi propia subclase personalizada de UICollectionViewLayout?
Rivera

2
@Rivera, tu respuesta es más razonable y suena correcta. He leído esta sugerencia en muchos lugares. También podría compartir algún enlace para mostrar o tute sobre cómo lograr eso o al menos algunos consejos. TIA
thesummersign

3
Su respuesta asume que está comenzando desde cero y que aún no tiene una gran vista de tabla compleja en la que necesita encajar. La pregunta específicamente sobre cómo poner una vista de colección en una vista de tabla. En mi opinión, su respuesta no es una "respuesta"
xaphod

2
Esta es una mala respuesta. Tener vistas de colección dentro de las vistas de tabla es algo tan común que casi todas las aplicaciones lo hacen.
crashoverride777

10

La respuesta de Pablo Romeu anterior ( https://stackoverflow.com/a/33364092/2704206 ) me ayudó enormemente con mi problema. Sin embargo, tuve que hacer algunas cosas de manera diferente para que esto funcionara para mi problema. En primer lugar, no tuve que llamar layoutIfNeeded()tan a menudo. Solo tuve que llamarlo collectionViewen la systemLayoutSizeFittingfunción.

En segundo lugar, tenía restricciones de diseño automático en mi vista de colección en la celda de vista de tabla para darle algo de relleno. Así que tuve que restar los márgenes inicial y final del targetSize.widthal establecer el collectionView.frameancho de. También tuve que agregar los márgenes superior e inferior a la CGSizealtura del valor de retorno .

Para obtener estas constantes de restricción, tuve la opción de crear salidas para las restricciones, codificar sus constantes o buscarlas por un identificador. Decidí optar por la tercera opción para hacer que mi clase de celda de vista de tabla personalizada sea fácilmente reutilizable. Al final, esto fue todo lo que necesitaba para que funcionara:

class CollectionTableViewCell: UITableViewCell {

    // MARK: -
    // MARK: Properties
    @IBOutlet weak var collectionView: UICollectionView! {
        didSet {
            collectionViewLayout?.estimatedItemSize = CGSize(width: 1, height: 1)
            selectionStyle = .none
        }
    }

    var collectionViewLayout: UICollectionViewFlowLayout? {
        return collectionView.collectionViewLayout as? UICollectionViewFlowLayout
    }

    // MARK: -
    // MARK: UIView functions
    override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
        collectionView.layoutIfNeeded()

        let topConstraintConstant = contentView.constraint(byIdentifier: "topAnchor")?.constant ?? 0
        let bottomConstraintConstant = contentView.constraint(byIdentifier: "bottomAnchor")?.constant ?? 0
        let trailingConstraintConstant = contentView.constraint(byIdentifier: "trailingAnchor")?.constant ?? 0
        let leadingConstraintConstant = contentView.constraint(byIdentifier: "leadingAnchor")?.constant ?? 0

        collectionView.frame = CGRect(x: 0, y: 0, width: targetSize.width - trailingConstraintConstant - leadingConstraintConstant, height: 1)

        let size = collectionView.collectionViewLayout.collectionViewContentSize
        let newSize = CGSize(width: size.width, height: size.height + topConstraintConstant + bottomConstraintConstant)
        return newSize
    }
}

Como función auxiliar para recuperar una restricción por identificador, agrego la siguiente extensión:

extension UIView {
    func constraint(byIdentifier identifier: String) -> NSLayoutConstraint? {
        return constraints.first(where: { $0.identifier == identifier })
    }
}

NOTA: Deberá establecer el identificador en estas restricciones en su guión gráfico o donde sea que se creen. A menos que tengan una constante 0, no importa. Además, como en la respuesta de Pablo, deberá usarlo UICollectionViewFlowLayoutcomo diseño para la vista de su colección. Finalmente, asegúrese de vincular collectionViewIBOutlet a su guión gráfico.

Con la celda de vista de tabla personalizada anterior, ahora puedo subclasificarla en cualquier otra celda de vista de tabla que necesite una vista de colección y hacer que implemente los protocolos UICollectionViewDelegateFlowLayouty UICollectionViewDataSource. ¡Espero que esto sea útil para alguien más!


5

Leí todas las respuestas. Esto parece servir para todos los casos.

override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
        collectionView.layoutIfNeeded()
        collectionView.frame = CGRect(x: 0, y: 0, width: targetSize.width , height: 1)
        return collectionView.collectionViewLayout.collectionViewContentSize
}

3

Una alternativa a la solución de Pablo Romeu es personalizar UICollectionView en sí, en lugar de hacer el trabajo en la celda de vista de tabla.

El problema subyacente es que, de forma predeterminada, una vista de colección no tiene un tamaño intrínseco y, por lo tanto, no puede informar el diseño automático de las dimensiones a utilizar. Puede remediarlo creando una subclase personalizada que devuelva un tamaño intrínseco útil.

Cree una subclase de UICollectionView y anule los siguientes métodos

override func intrinsicContentSize() -> CGSize {
    self.layoutIfNeeded()

    var size = super.contentSize
    if size.width == 0 || size.height == 0 {
        // return a default size
        size = CGSize(width: 600, height:44)
    }

    return size
 }

override func reloadData() {
    super.reloadData()
    self.layoutIfNeeded()
    self.invalidateIntrinsicContentSize()
}

(También debe anular los métodos relacionados: reloadSections, reloadItemsAtIndexPaths de manera similar a reloadData ())

Llamar a layoutIfNeeded obliga a la vista de colección a recalcular el tamaño del contenido que luego se puede usar como el nuevo tamaño intrínseco.

Además, debe manejar explícitamente los cambios en el tamaño de la vista (por ejemplo, en la rotación del dispositivo) en el controlador de vista de tabla

    override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator)
{
    super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
    dispatch_async(dispatch_get_main_queue()) {
        self.tableView.reloadData()
    }
}

hermano, ¿podrías ayudarme con mi problema stackoverflow.com/questions/44650058/… ?
Mayo Phyu

3

Recientemente me enfrenté al mismo problema y casi probé todas las soluciones en las respuestas, algunas de ellas funcionaron y otras no, mi principal preocupación sobre el enfoque de @PabloRomeu es que si tiene otros contenidos en la celda (además de la vista de colección) tendrá que calcular sus alturas y las alturas de sus restricciones y devolver el resultado para obtener el diseño automático correcto y no me gusta calcular las cosas manualmente en mi código. Así que aquí está la solución que funcionó bien para mí sin hacer ningún cálculo manual en mi código.

en la cellForRow:atIndexPathvista de tabla hago lo siguiente:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    //do dequeue stuff
    //initialize the the collection view data source with the data
    cell.frame = CGRect.zero
    cell.layoutIfNeeded()
    return cell
}

Creo que lo que sucede aquí es que obligo a la celda de la vista de tabla a ajustar su altura después de que se haya calculado la altura de la vista de la colección. (después de proporcionar la fecha de collectionView a la fuente de datos)


2

Pondría un método estático en la clase de vista de colección que devolverá un tamaño basado en el contenido que tendrá. Luego use ese método en el heightForRowAtIndexPathpara devolver el tamaño adecuado.

También tenga en cuenta que puede tener un comportamiento extraño cuando inserta este tipo de viewControllers. Lo hice una vez y tuve algunos problemas de memoria extraños que nunca resolví.


Para mí, estaba tratando de incrustar un controlador de vista de tabla dentro de los controladores de vista de colección. Cada vez que se reutiliza una celda, termina recordando algunas restricciones de diseño automático que les apliqué. Fue muy complicado y, en última instancia, fue una idea terrible. Creo que solo usaré una vista de colección única en su lugar.
fatuhoku

2

Quizás mi variante sea útil; He estado decidiendo esta tarea durante las últimas dos horas. No pretendo que sea 100% correcto u óptimo, pero mi habilidad es muy pequeña todavía y me gustaría escuchar comentarios de expertos. Gracias. Una nota importante: esto funciona para tablas estáticas, está especificado por mi trabajo actual. Entonces, todo lo que uso es viewWillLayoutSubviews de tableView . Y un poquito más.

private var iconsCellHeight: CGFloat = 500 

func updateTable(table: UITableView, withDuration duration: NSTimeInterval) {
    UIView.animateWithDuration(duration, animations: { () -> Void in
        table.beginUpdates()
        table.endUpdates()
    })
}

override func viewWillLayoutSubviews() {
    if let iconsCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 1)) as? CategoryCardIconsCell {
        let collectionViewContentHeight = iconsCell.iconsCollectionView.contentSize.height
        if collectionViewContentHeight + 17 != iconsCellHeight {
            iconsCellHeight = collectionViewContentHeight + 17
            updateTable(tableView, withDuration: 0.2)
        }
    }
}

override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    switch (indexPath.section, indexPath.row) {
        case ...
        case (1,0):
            return iconsCellHeight
        default:
            return tableView.rowHeight
    }
}
  1. Sé que collectionView se encuentra en la primera fila de la segunda sección;
  2. Deje que la altura de la fila sea 17 p. más grande que la altura de su contenido;
  3. iconsCellHeight es un número aleatorio cuando se inicia el programa (lo sé, en el formato vertical tiene que ser exactamente 392, pero no es importante). Si el contenido de collectionView + 17 no es igual a este número, cambie su valor. La próxima vez en esta situación la condición da FALSO;
  4. Después de todo, actualice el archivo tableView. En mi caso, es la combinación de dos operaciones (para una buena actualización de las filas extendidas);
  5. Y, por supuesto, en heightForRowAtIndexPath agregue una fila al código.

2

El enfoque más fácil que se me ocurrió, hasta ahora, Créditos para la respuesta de @igor anterior,

En su clase tableviewcell simplemente inserte esto

override func layoutSubviews() {
    self.collectionViewOutlet.constant = self.postPoll.collectionViewLayout.collectionViewContentSize.height
}

y, por supuesto, cambie la vista de la colección con su salida en la clase de la celda


1
La altura de la celda no se calcula correctamente
Nik Kov

2

Tengo una idea de la publicación de @Igor e invierto mi tiempo en esto para mi proyecto con swift

Solo pasa esto en tu

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    //do dequeue stuff
    cell.frame = tableView.bounds
    cell.layoutIfNeeded()
    cell.collectionView.reloadData()
    cell.collectionView.heightAnchor.constraint(equalToConstant: cell.collectionView.collectionViewLayout.collectionViewContentSize.height)
    cell.layoutIfNeeded()
    return cell
}

Además: si ve su UICollectionView entrecortado al cargar celdas.

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {       
  //do dequeue stuff
  cell.layer.shouldRasterize = true
  cell.layer.rasterizationScale = UIScreen.main.scale
  return cell
}

0

La solución de Pablo no me funcionó muy bien, tuve efectos visuales extraños (la vista de colección no se ajustaba correctamente).

Lo que funcionó fue ajustar la restricción de altura de collectionView (como NSLayoutConstraint) a collectionView contentSize durante layoutSubviews(). Este es el método llamado cuando se aplica el diseño automático a la celda.

// Constraint on the collectionView height in the storyboard. Priority set to 999.
@IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint!


// Method called by autolayout to layout the subviews (including the collectionView).
// This is triggered with 'layoutIfNeeded()', or by the viewController
// (happens between 'viewWillLayoutSubviews()' and 'viewDidLayoutSubviews()'.
override func layoutSubviews() {

    collectionViewHeightConstraint.constant = collectionView.contentSize.height
    super.layoutSubviews()
}

// Call `layoutIfNeeded()` when you update your UI from the model to trigger 'layoutSubviews()'
private func updateUI() {
    layoutIfNeeded()
}

-3

En su UITableViewDelegate:

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return ceil(itemCount/4.0f)*collectionViewCellHeight;
}

Sustituya itemCount y CollectionViewCellHeight con los valores reales. Si tiene una matriz de matrices, itemCount podría ser:

self.items[indexPath.row].count

O lo que sea.


Creo que la pregunta principal es cómo calcular collectionViewCellHeight antes de renderizar.
thesummersign

-3

1.Crear celda ficticia.
Utilice el método collectionViewContentSize en UICollectionViewLayout de UICollectionView utilizando datos actuales.


Debería ser la mejor respuesta ... ya que no hay posibilidad de establecer la altura de la celda con 'uicollectionview' sin poner el contenido en la celda ficticia, obtener la altura y luego configurarlo nuevamente en la celda adecuada
Bartłomiej Semańczyk

¿Cómo uso ese método?
xtrinch

Agrega un ejemplo.
Nik Kov

-5

Se puede calcular la altura de la colección basada en sus propiedades como itemSize, sectionInset, minimumLineSpacing, minimumInteritemSpacing, si su collectionViewCellcuenta con el borde de una regla.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.