Las alturas de celda dinámica de UITableView solo se corrigen después de un poco de desplazamiento

117

Tengo una UITableViewcon un personalizado UITableViewCelldefinido en un guión gráfico usando diseño automático. La celda tiene varias multilíneas UILabels.

Los UITableViewparece altos de celdas adecuadamente calcular, pero para las primeras células que la altura no se divide adecuadamente entre las etiquetas. Después de desplazarse un poco, todo funciona como se esperaba (incluso las celdas que inicialmente eran incorrectas).

- (void)viewDidLoad {
    [super viewDidLoad]
    // ...
    self.tableView.rowHeight = UITableViewAutomaticDimension;
}


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    TableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"TestCell"];
    // ...
    // Set label.text for variable length string.
    return cell;
}

¿Hay algo que me pueda faltar, que esté causando que el diseño automático no pueda hacer su trabajo las primeras veces?

Creé un proyecto de muestra que demuestra este comportamiento.

Proyecto de muestra: Vista superior de la tabla del proyecto de muestra en la primera carga. Proyecto de muestra: las mismas celdas después de desplazarse hacia abajo y hacia arriba.

blackp
fuente
1
También estoy enfrentando este problema, pero la respuesta a continuación no me funciona, cualquier ayuda en esto
Shangari C
1
enfrenta el mismo problema al agregar layoutIfNeeded for cell no me funciona también? más sugerencias
Max
1
Gran lugar. Esto sigue siendo un problema en 2019: /
Fattie
Encontré lo que me ayudó en el siguiente enlace: es una discusión bastante completa de las celdas de vista de tabla de altura variable: stackoverflow.com/a/18746930/826946
Andy Weinstein

Respuestas:

139

No sé que esto esté claramente documentado o no, pero agregar [cell layoutIfNeeded]antes de devolver la celda resuelve su problema.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    TableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"TestCell"];
    NSUInteger n1 = firstLabelWordCount[indexPath.row];
    NSUInteger n2 = secondLabelWordCount[indexPath.row];
    [cell setNumberOfWordsForFirstLabel:n1 secondLabel:n2];

    [cell layoutIfNeeded]; // <- added

    return cell;
}
rintaro
fuente
3
Sin embargo, me pregunto, ¿por qué solo es necesario la primera vez? Funciona igual de bien si pongo la llamada a -layoutIfNeededen el celular -awakeFromNib. Preferiría llamar solo layoutIfNeededdonde sé por qué es necesario.
blackp
2
¡Trabajó para mi! ¡Y me encantaría saber por qué es necesario!
Mustafa
No funcionó si renderiza la clase de tamaño, que es diferente de cualquier X cualquiera
Andrei Konstantinov
@AndreyKonstantinov Sí, no funciona con la clase de tamaño, ¿cómo hago para que funcione con la clase de tamaño?
z22
Funciona sin clase de tamaño, ¿alguna solución con clase de tamaño?
Yuvrajsinh
38

Esto funcionó para mí cuando otras soluciones similares no lo hicieron:

override func didMoveToSuperview() {
    super.didMoveToSuperview()
    layoutIfNeeded()
}

Esto parece un error real ya que estoy muy familiarizado con AutoLayout y cómo usar UITableViewAutomaticDimension, sin embargo, ocasionalmente me encuentro con este problema. Me alegro de haber encontrado finalmente algo que funciona como solución.

Michael Peterson
fuente
1
Mejor que la respuesta aceptada, ya que solo se llama cuando la celda se carga desde el xib en lugar de cada pantalla de celda. También hay muchas menos líneas de código.
Robert Wagstaff
3
No olvides llamarsuper.didMoveToSuperview()
cicerocamargo
@cicerocamargo Apple dice sobre didMoveToSuperview: "La implementación predeterminada de este método no hace nada".
Ivan Smetanin
4
@IvanSmetanin, no importa si la implementación predeterminada no hace nada, aún debe llamar al súper método, ya que en realidad podría hacer algo en el futuro.
TNguyen
(Por cierto, DEFINITIVAMENTE deberías llamar a super. En ese caso. Nunca he visto un proyecto en el que el equipo en general no subclase más de una vez las celdas, así que, por supuesto, debes asegurarte de recogerlas todas. Y como número separado justo lo que dijo TNguyen.)
Fattie
22

Adición [cell layoutIfNeeded]en cellForRowAtIndexPathno funciona para células que se desplazan inicialmente fuera de la vista.

Tampoco precederlo con [cell setNeedsLayout].

Todavía tiene que desplazar ciertas celdas hacia afuera y volver a verlas para que cambien de tamaño correctamente.

Esto es bastante frustrante ya que la mayoría de los desarrolladores tienen celdas de tipo dinámico, autodiseño y de tamaño propio funcionando correctamente, excepto en este caso molesto. Este error afecta a todos mis controladores de vista de tabla "más altos".

Guión
fuente
Lo mismo para mi. cuando me desplazo por primera vez, el tamaño de la celda es 44px. si me desplazo para hacer que la celda se pierda de vista y regrese, tiene el tamaño adecuado. ¿Encontraste alguna solución?
Max
Gracias @ ashy_32bit, esto también lo resolvió para mí.
ImpurestClub
Parecería ser un error simple, @Scenario, ¿verdad? cosas horribles.
Fattie
3
Usar en [cell layoutSubviews]lugar de layoutIfNeeded podría ser una posible solución. Consulte stackoverflow.com/a/33515872/1474113
ypresto
14

Tuve la misma experiencia en uno de mis proyectos.

¿Por qué sucede?

Celda diseñada en Storyboard con algo de ancho para algún dispositivo. Por ejemplo 400px. Por ejemplo, su etiqueta tiene el mismo ancho. Cuando se carga desde el guión gráfico, tiene un ancho de 400 px.

Aquí hay un problema:

tableView:heightForRowAtIndexPath: llamado antes del diseño de la celda, sus subvistas.

Entonces calculó la altura de la etiqueta y la celda con un ancho de 400px. Pero se ejecuta en un dispositivo con pantalla, por ejemplo, 320px. Y esta altura calculada automáticamente es incorrecta. El hecho de que la celda layoutSubviewsocurra solo después de tableView:heightForRowAtIndexPath: Incluso si configura preferredMaxLayoutWidthsu etiqueta manualmente layoutSubviewsno ayuda.

Mi solución:

1) Subclase UITableViewy anulación dequeueReusableCellWithIdentifier:forIndexPath:. Establezca el ancho de la celda igual al ancho de la tabla y fuerce el diseño de la celda.

- (UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [super dequeueReusableCellWithIdentifier:identifier forIndexPath:indexPath];
    CGRect cellFrame = cell.frame;
    cellFrame.size.width = self.frame.size.width;
    cell.frame = cellFrame;
    [cell layoutIfNeeded];
    return cell;
}

2) Subclase UITableViewCell. Configure preferredMaxLayoutWidthmanualmente sus etiquetas en formato layoutSubviews. También necesita un diseño manual contentView, porque no se distribuye automáticamente después del cambio del marco de la celda (no sé por qué, pero lo es)

- (void)layoutSubviews {
    [super layoutSubviews];
    [self.contentView layoutIfNeeded];
    self.yourLongTextLabel.preferredMaxLayoutWidth = self.yourLongTextLabel.width;
}
Vitaliy Gozhenko
fuente
1
Cuando me encontré con este problema, también necesitaba asegurarme de que el marco de la vista de tabla era correcto, así que llamé a [self.tableview layoutIfNeeded] antes de que se completara la vista de tabla (en viewDidLoad).
GK100
Nunca me detuve a pensar que tenía que ver con un ancho incorrecto. cell.frame.size.width = tableview.frame.widthluego cell.layoutIfNeeded()en la cellForRowAtfunción hizo el truco para mí
Cam Connor
10

Tengo un problema similar, en la primera carga, no se calculó la altura de la fila, pero después de desplazarme un poco o ir a otra pantalla y vuelvo a esta pantalla, se calculan las filas. En la primera carga, mis artículos se cargan desde Internet y en la segunda carga, mis artículos se cargan primero desde Core Data y se vuelven a cargar desde Internet, y noté que la altura de las filas se calcula en la recarga desde Internet. Así que noté que cuando se llama a tableView.reloadData () durante la animación de segue (el mismo problema con push y present segue), no se calculó la altura de la fila. Así que oculté la vista de tabla en la inicialización de la vista y puse un cargador de actividad para evitar un efecto desagradable para el usuario y llamé a tableView.reloadData después de 300ms y ahora el problema está resuelto. Creo que es un error de UIKit, pero esta solución funciona.

Puse estas líneas (Swift 3.0) en mi controlador de finalización de carga de artículos

DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300), execute: {
        self.tableView.isHidden = false
        self.loader.stopAnimating()
        self.tableView.reloadData()
    })

Esto explica por qué, para algunas personas, poner un reloadData en el diseño.

alvinmeimoun
fuente
Este es el único WA que también funcionó para mí. Excepto que no uso isHidden, pero establezco el alfa de la tabla en 0 al agregar la fuente de datos, luego en 1 después de recargar.
PJ_Finnegan
6

ninguna de las soluciones anteriores funcionó para mí, lo que funcionó es esta receta de una magia: llámalos en este orden:

tableView.reloadData()
tableView.layoutIfNeeded() tableView.beginUpdates() tableView.endUpdates()

mis datos de tableView se completan desde un servicio web, en la devolución de llamada de la conexión escribo las líneas anteriores.

JAHelia
fuente
1
Para Swift 5 esto funcionó incluso en la vista de colección, Dios te bendiga, hermano.
Govani Dhruv Vijaykumar
Para mí, esto devolvió la altura y el diseño correctos de la celda, pero no tenía datos
Darrow Hartman
Pruebe setNeedsDisplay () después de esas líneas
JAHelia
5

En mi caso, la última línea de UILabel se truncó cuando se mostró la celda por primera vez. Ocurrió de forma bastante aleatoria y la única forma de dimensionarlo correctamente era desplazar la celda fuera de la vista y traerla de vuelta. Probé todas las soluciones posibles mostradas hasta ahora (layoutIfNeeded..reloadData) pero nada funcionó para mí. El truco consistía en establecer "Autoencogimiento" en Escala de fuente mínima (0,5 para mí). Darle una oportunidad

Claus
fuente
esto funcionó para mí, sin aplicar ninguna de las otras soluciones enumeradas anteriormente. Gran hallazgo.
mckeejm
2
Pero esto encoge automáticamente el texto ... ¿por qué querría uno tener diferentes tamaños de texto en una vista de tabla? Parece terrible.
lucius degeer
5

Agregue una restricción para todo el contenido dentro de una celda personalizada de vista de tabla, luego estime la altura de fila de vista de tabla y establezca la altura de fila en dimensión automática con una carga viewdid:

    override func viewDidLoad() {
    super.viewDidLoad()

    tableView.estimatedRowHeight = 70
    tableView.rowHeight = UITableViewAutomaticDimension
}

Para solucionar ese problema de carga inicial, aplique el método layoutIfNeeded en una celda de vista de tabla personalizada:

class CustomTableViewCell: UITableViewCell {

override func awakeFromNib() {
    super.awakeFromNib()
    self.layoutIfNeeded()
    // Initialization code
}
}
bikram sapkota
fuente
Es importante establecer estimatedRowHeightun valor> 0 y no UITableViewAutomaticDimension(que es -1), de lo contrario, la altura de fila automática no funcionará.
PJ_Finnegan
5

Probé la mayoría de las respuestas a esta pregunta y no pude hacer que ninguna funcionara. La única solución funcional que encontré fue agregar lo siguiente a mi UITableViewControllersubclase:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    UIView.performWithoutAnimation {
        tableView.beginUpdates()
        tableView.endUpdates()
    }
}

Se UIView.performWithoutAnimationrequiere la llamada; de lo contrario, verá la animación de vista de tabla normal a medida que se carga el controlador de vista.

Chris Vig
fuente
funcionando y definitivamente mejor que invocar reloadData dos veces
Jimmy George Thomas
2
Magia negra. Hacerlo viewWillAppearno funcionó para mí, pero hacerlo viewDidAppearsí.
Martin
Esta solución 'sí' funcionó para mí cuando se implementó en viewDidAppear. De ninguna manera es óptimo para la experiencia del usuario ya que el contenido de la tabla salta a medida que ocurre el tamaño correcto.
richardpiazza
Increíble, he probado muchos ejemplos pero falla, eres un demonio
Shakeel Ahmed
Igual que @martin
AJ Hernandez
3

llamar cell.layoutIfNeeded()adentro cellForRowAtfuncionó para mí en ios 10 e ios 11, pero no en ios 9.

para que esto funcione también en ios 9, llamo cell.layoutSubviews()y funcionó.

mnemónico23
fuente
2

Adjuntando captura de pantalla para tu referenciaPara mí, ninguno de estos enfoques funcionó, pero descubrí que la etiqueta tenía un Preferred Widthconjunto explícito en Interface Builder. Eliminar eso (desmarcar "Explícito") y luego usar UITableViewAutomaticDimensionfuncionó como se esperaba.

Jack James
fuente
2

Probé todas las soluciones en esta página, pero desmarcar las clases de tamaño de uso y luego verificarlas nuevamente resolvió mi problema.

Editar: desmarcar las clases de tamaño causa muchos problemas en el guión gráfico, así que probé otra solución. Completé mi vista de tabla en los métodos viewDidLoady del controlador de mi vista viewWillAppear. Esto resolvió mi problema.

ACengiz
fuente
1

Tengo el problema con el cambio de tamaño de la etiqueta, así que solo
necesito hacer chatTextLabel.text = chatMessage.message chatTextLabel? .UpdateConstraints () después de configurar el texto

// código completo

func setContent() {
    chatTextLabel.text = chatMessage.message
    chatTextLabel?.updateConstraints()

    let labelTextWidth = (chatTextLabel?.intrinsicContentSize().width) ?? 0
    let labelTextHeight = chatTextLabel?.intrinsicContentSize().height

    guard labelTextWidth < originWidth && labelTextHeight <= singleLineRowheight else {
      trailingConstraint?.constant = trailingConstant
      return
    }
    trailingConstraint?.constant = trailingConstant + (originWidth - labelTextWidth)

  }
Svitlana
fuente
1

En mi caso, estaba actualizando en otro ciclo. Entonces la altura de tableViewCell se actualizó después de que se estableció labelText. Eliminé el bloque asincrónico.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
     let cell = tableView.dequeueReusableCell(withIdentifier:Identifier, for: indexPath) 
     // Check your cycle if update cycle is same or not
     // DispatchQueue.main.async {
        cell.label.text = nil
     // }
}
Den Jo
fuente
También tengo un bloque asincrónico, pero ¿es necesario? ¿No hay alternativa?
Nazar Medeiros
1

Solo asegúrese de no configurar el texto de la etiqueta en el método delegado 'willdisplaycell' de la vista de tabla. Establezca el texto de la etiqueta en el método delegado 'cellForRowAtindexPath' para el cálculo dinámico de la altura.

De nada :)

Pranav Rivankar
fuente
1

En mi caso, una vista de pila en la celda estaba causando el problema. Aparentemente es un error. Una vez que lo eliminé, el problema se resolvió.

Guilherme Carvalho
fuente
1
Probé
1

El problema es que las celdas iniciales se cargan antes de que tengamos una altura de fila válida. La solución alternativa es forzar la recarga de la tabla cuando aparece la vista.

- (void)viewDidAppear:(BOOL)animated
{
  [super viewDidAppear:animated];
  [self.tableView reloadData];
}
alicanozkara
fuente
esta fue la mejor solución que me ayudó. No corrigió el 100% de la celda, pero hasta el 80% estaba tomando su tamaño real. Muchas gracias
TheTravloper
1

Solo para iOS 12+, 2019 en adelante ...

Un ejemplo continuo de la incompetencia extraña ocasional de Apple, donde los problemas continúan literalmente durante años.

Parece ser el caso que

        cell.layoutIfNeeded()
        return cell

lo arreglará. (Está perdiendo algo de rendimiento, por supuesto).

Así es la vida con Apple.

Fattie
fuente
0

En mi caso, el problema con la altura de la celda se produce después de que se carga la vista de la tabla inicial y se lleva a cabo una acción del usuario (tocar un botón en una celda que tiene el efecto de cambiar la altura de la celda). No he podido hacer que la celda cambie su altura a menos que lo haga:

[self.tableView reloadData];

lo intenté

[cell layoutIfNeeded];

pero eso no funcionó.

Chris Prince
fuente
0

En Swift 3. Tuve que llamar a self.layoutIfNeeded () cada vez que actualizo el texto de la celda reutilizable.

import UIKit
import SnapKit

class CommentTableViewCell: UITableViewCell {

    static let reuseIdentifier = "CommentTableViewCell"

    var comment: Comment! {
        didSet {
            textLbl.attributedText = comment.attributedTextToDisplay()
            self.layoutIfNeeded() //This is a fix to make propper automatic dimentions (height).
        }
    }

    internal var textLbl = UILabel()

    override func layoutSubviews() {
        super.layoutSubviews()

        if textLbl.superview == nil {
            textLbl.numberOfLines = 0
            textLbl.lineBreakMode = .byWordWrapping
            self.contentView.addSubview(textLbl)
            textLbl.snp.makeConstraints({ (make) in
                make.left.equalTo(contentView.snp.left).inset(10)
                make.right.equalTo(contentView.snp.right).inset(10)
                make.top.equalTo(contentView.snp.top).inset(10)
                make.bottom.equalTo(contentView.snp.bottom).inset(10)
            })
        }
    }
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let comment = comments[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: CommentTableViewCell.reuseIdentifier, for: indexPath) as! CommentTableViewCell
        cell.selectionStyle = .none
        cell.comment = comment
        return cell
    }

commentsTableView.rowHeight = UITableViewAutomaticDimension
    commentsTableView.estimatedRowHeight = 140
Naloiko Eugene
fuente
0

Ninguna de las soluciones anteriores funcionó, pero la siguiente combinación de sugerencias sí.

Tuve que agregar lo siguiente en viewDidLoad ().

DispatchQueue.main.async {

        self.tableView.reloadData()

        self.tableView.setNeedsLayout()
        self.tableView.layoutIfNeeded()

        self.tableView.reloadData()

    }

La combinación anterior de reloadData, setNeedsLayout y layoutIfNeeded funcionó, pero no ninguna otra. Sin embargo, podría ser específico de las celdas del proyecto. Y sí, tuve que invocar reloadData dos veces para que funcione.

También configure lo siguiente en viewDidLoad

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = MyEstimatedHeight

En tableView (_ tableView: UITableView, cellForRowAt indexPath: IndexPath)

cell.setNeedsLayout()
cell.layoutIfNeeded() 
Jimmy George Thomas
fuente
Mi similar reloadData/beginUpdates/endUpdates/reloadDatatambién está funcionando; reloadData debe ser llamado por segunda vez. No es necesario envolverlo async.
rayo
0

Me encontré con este problema y lo arreglaron moviendo mi punto de vista / etiqueta de código de inicialización DE tableView(willDisplay cell:)A tableView(cellForRowAt:).

levantado y vidriado
fuente
que NO es la forma recomendada de inicializar una celda. willDisplaytendrá un mejor rendimiento que cellForRowAt. Use el último solo para instanciar la celda correcta.
Martin
2 años después, estoy leyendo ese viejo comentario que escribí. Este fue un mal consejo. Incluso si willDisplaytiene un mejor rendimiento, se recomienda inicializar la interfaz de usuario de su celda cellForRowAtcuando el diseño es automático. De hecho, el diseño lo calcula UIKit después de cellForRowAt et antes de willDisplay. Entonces, si la altura de su celda depende de su contenido, inicialice el contenido de la etiqueta (o lo que sea) en cellForRowAt.
Martin
-1
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{


//  call the method dynamiclabelHeightForText
}

use el método anterior que devuelve la altura de la fila de forma dinámica. Y asigne la misma altura dinámica a la etiqueta que está utilizando.

-(int)dynamiclabelHeightForText:(NSString *)text :(int)width :(UIFont *)font
{

    CGSize maximumLabelSize = CGSizeMake(width,2500);

    CGSize expectedLabelSize = [text sizeWithFont:font
                                constrainedToSize:maximumLabelSize
                                    lineBreakMode:NSLineBreakByWordWrapping];


    return expectedLabelSize.height;


}

Este código le ayuda a encontrar la altura dinámica del texto que se muestra en la etiqueta.

Ramesh Muthe
fuente
En realidad, Ramesh, esto no debería ser necesario siempre que se establezcan las propiedades tableView.estimatedRowHeight y tableView.rowHeight = UITableViewAutomaticDimension. Además, asegúrese de que la celda personalizada tenga las restricciones adecuadas en los diversos widgets y contentView.
BonanzaDriver