Les hauteurs de cellule dynamiques UITableView ne sont correctes qu'après un certain défilement

117

J'ai un UITableViewavec un personnalisé UITableViewCelldéfini dans un storyboard en utilisant la mise en page automatique. La cellule a plusieurs multilignes UILabels.

Le UITableViewsemble calculer correctement la hauteur des cellules, mais pour les premières cellules, cette hauteur n'est pas correctement répartie entre les étiquettes. Après avoir fait défiler un peu, tout fonctionne comme prévu (même les cellules initialement incorrectes).

- (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;
}

Y a-t-il quelque chose qui me manque, qui empêche la mise en page automatique de faire son travail les premières fois?

J'ai créé un exemple de projet qui illustre ce comportement.

Exemple de projet: vue de dessus du tableau à partir d'un exemple de projet lors du premier chargement. Exemple de projet: mêmes cellules après défilement vers le bas et sauvegarde.

blackp
la source
1
Je suis également confronté à ce problème, mais la réponse ci-dessous ne fonctionne pas pour moi, toute aide à ce sujet
Shangari C
1
face au même problème l'ajout de mise en page Si nécessaire pour la cellule ne fonctionne pas aussi bien? toutes autres suggestions
Max
1
Super endroit. C'est encore un problème en 2019: /
Fattie
J'ai trouvé ce qui m'a aidé dans le lien suivant - c'est une discussion assez complète sur les cellules de vue de table à hauteur variable: stackoverflow.com/a/18746930/826946
Andy Weinstein

Réponses:

139

Je ne sais pas que cela est clairement documenté ou non, mais ajouter [cell layoutIfNeeded]avant de retourner la cellule résout votre problème.

- (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
la source
3
Je me demande cependant pourquoi est-ce seulement nécessaire la première fois? Cela fonctionne aussi bien si je mets l'appel -layoutIfNeededdans la cellule -awakeFromNib. Je préfère n'appeler que layoutIfNeededlà où je sais pourquoi c'est nécessaire.
blackp
2
A travaillé pour moi! Et j'aimerais savoir pourquoi c'est nécessaire!
Mustafa
Cela ne fonctionnait pas si vous rendiez une classe de taille différente de n'importe quel X
Andrei Konstantinov
@AndreyKonstantinov Yup cela ne fonctionne pas avec la classe de taille, comment puis-je le faire fonctionner avec la classe de taille?
z22
Cela fonctionne sans classe de taille, une solution avec classe de taille?
Yuvrajsinh
38

Cela a fonctionné pour moi lorsque d'autres solutions similaires n'ont pas fonctionné:

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

Cela semble être un bogue réel car je suis très familier avec AutoLayout et comment utiliser UITableViewAutomaticDimension, mais je rencontre encore occasionnellement ce problème. Je suis content d'avoir enfin trouvé quelque chose qui fonctionne comme une solution de contournement.

Michael Peterson
la source
1
Mieux que la réponse acceptée car elle n'est appelée que lorsque la cellule est chargée à partir du xib au lieu de chaque affichage de cellule. Beaucoup moins de lignes de code aussi.
Robert Wagstaff
3
N'oubliez pas d'appelersuper.didMoveToSuperview()
cicerocamargo
@cicerocamargo Apple dit à propos de didMoveToSuperview: "L'implémentation par défaut de cette méthode ne fait rien."
Ivan Smetanin
4
@IvanSmetanin peu importe si l'implémentation par défaut ne fait rien, vous devez toujours appeler la super méthode car elle pourrait en fait faire quelque chose dans le futur.
TNguyen
(Juste au fait, vous devriez DÉFINITIVEMENT appeler super. Sur celui-là. Je n'ai jamais vu de projet où l'équipe dans son ensemble ne sous-classe pas plus d'une fois les cellules, donc, bien sûr, vous devez être sûr de les prendre toutes. ET en tant que question distincte juste ce que TNguyen a dit.)
Fattie
22

Ajout [cell layoutIfNeeded]dans cellForRowAtIndexPathne fonctionne pas pour les cellules qui sont d' abord hors de défilaient vue.

Ni le préfacer avec [cell setNeedsLayout].

Vous devez toujours faire défiler certaines cellules et les remettre en vue pour qu'elles se redimensionnent correctement.

C'est assez frustrant car la plupart des développeurs ont des cellules de type dynamique, de mise en page automatique et d'auto-dimensionnement fonctionnant correctement - à l'exception de ce cas ennuyeux. Ce bogue affecte tous mes contrôleurs de vue de table "plus hauts".

Scénario
la source
Pareil pour moi. lorsque je fais défiler pour la première fois, la taille de la cellule est de 44 px. si je fais défiler pour rendre la cellule hors de vue et que je reviens, elle est dimensionnée correctement. Avez-vous trouvé une solution?
Max
Merci @ ashy_32bit, cela a également résolu le problème pour moi.
ImpurestClub
cela semble être un simple bug, @Scenario, n'est-ce pas? trucs horribles.
Fattie
3
Utiliser [cell layoutSubviews]au lieu de layoutIfNeeded pourrait être une solution possible. Reportez-vous à stackoverflow.com/a/33515872/1474113
ypresto
14

J'ai eu la même expérience dans l'un de mes projets.

Pourquoi ça arrive?

Cellule conçue dans Storyboard avec une certaine largeur pour certains appareils. Par exemple 400px. Par exemple, votre étiquette a la même largeur. Lorsqu'il se charge à partir du storyboard, il a une largeur de 400px.

Voici un problème:

tableView:heightForRowAtIndexPath: appelé avant la mise en page de la cellule, ses sous-vues.

Il a donc calculé la hauteur de l'étiquette et de la cellule d'une largeur de 400 px. Mais vous exécutez sur un appareil avec écran, par exemple, 320px. Et cette hauteur calculée automatiquement est incorrecte. Juste parce que les cellules ne layoutSubviewsse produisent qu'après tableView:heightForRowAtIndexPath: Même si vous définissez preferredMaxLayoutWidthmanuellement votre étiquette, layoutSubviewscela n'aide pas.

Ma solution:

1) Sous UITableView- classe et remplacement dequeueReusableCellWithIdentifier:forIndexPath:. Définissez la largeur de cellule égale à la largeur du tableau et forcez la disposition de la cellule.

- (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) Sous-classe UITableViewCell. Définissez preferredMaxLayoutWidthmanuellement vos étiquettes dans layoutSubviews. Vous avez également besoin d'une mise en page manuelle contentView, car elle n'est pas mise en page automatiquement après le changement de cadre de cellule (je ne sais pas pourquoi, mais c'est le cas)

- (void)layoutSubviews {
    [super layoutSubviews];
    [self.contentView layoutIfNeeded];
    self.yourLongTextLabel.preferredMaxLayoutWidth = self.yourLongTextLabel.width;
}
Vitaliy Gozhenko
la source
1
Lorsque j'ai rencontré ce problème, je devais également m'assurer que le cadre de la tableview était correct, j'ai donc appelé [self.tableview layoutIfNeeded] avant que la tableview ne soit remplie (dans viewDidLoad).
GK100
Je n'ai jamais cessé de penser que cela avait à voir avec une largeur incorrecte. cell.frame.size.width = tableview.frame.widthpuis cell.layoutIfNeeded()dans la cellForRowAtfonction a fait l'affaire pour moi
Cam Connor
10

J'ai un problème similaire, au premier chargement, la hauteur de ligne n'a pas été calculée mais après quelques défilement ou aller sur un autre écran et je reviens à cet écran, les lignes sont calculées. Au premier chargement, mes articles sont chargés à partir d'Internet et au deuxième chargement, mes articles sont d'abord chargés à partir de Core Data et rechargés à partir d'Internet et j'ai remarqué que la hauteur des lignes est calculée au rechargement d'Internet. J'ai donc remarqué que lorsque tableView.reloadData () est appelé pendant l'animation de segue (même problème avec push et present segue), la hauteur de ligne n'était pas calculée. J'ai donc caché la tableview lors de l'initialisation de la vue et mis un chargeur d'activité pour éviter un effet laid à l'utilisateur et j'appelle tableView.reloadData après 300 ms et maintenant le problème est résolu. Je pense que c'est un bogue UIKit mais cette solution de contournement fait l'affaire.

J'ai mis ces lignes (Swift 3.0) dans mon gestionnaire d'achèvement de chargement d'éléments

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

Cela explique pourquoi pour certaines personnes, mettez un reloadData dans layoutSubviews résolvent le problème

alvinmeimoun
la source
C'est le seul WA qui a fonctionné pour moi aussi. Sauf que je n'utilise pas isHidden, mais que je règle l'alpha de la table sur 0 lors de l'ajout de la source de données, puis sur 1 après le rechargement.
PJ_Finnegan
6

aucune des solutions ci-dessus n'a fonctionné pour moi, ce qui a fonctionné est cette recette de magie: appelez-les dans cet ordre:

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

mes données tableView sont remplies à partir d'un service Web, dans le rappel de la connexion, j'écris les lignes ci-dessus.

JAHelia
la source
1
Pour Swift 5, cela a fonctionné même en vue de la collection, que Dieu vous bénisse, mon frère.
Govani Dhruv Vijaykumar
Pour moi, cela a renvoyé la hauteur et la disposition correctes de la cellule, mais il ne contenait aucune donnée
Darrow Hartman
Essayez setNeedsDisplay () après ces lignes
JAHelia
5

Dans mon cas, la dernière ligne du UILabel était tronquée lorsque la cellule était affichée pour la première fois. Cela s'est produit de manière assez aléatoire et la seule façon de la dimensionner correctement était de faire défiler la cellule hors de la vue et de la ramener. J'ai essayé toutes les solutions possibles affichées jusqu'à présent (layoutIfNeeded..reloadData) mais rien n'a fonctionné pour moi. L'astuce consistait à régler "Autoshrink" sur Minimuum Font Scale (0,5 pour moi). Essaie

Claus
la source
cela a fonctionné pour moi, sans appliquer aucune des autres solutions énumérées ci-dessus. Super trouvaille.
mckeejm
2
Mais cela réduit automatiquement le texte ... pourquoi voudrait-on avoir différentes tailles de texte dans une vue de tableau? Ça a l'air terrible.
lucius degeer
5

Ajoutez une contrainte pour tout le contenu dans une cellule personnalisée de vue de tableau, puis estimez la hauteur de ligne de vue de tableau et définissez la hauteur de ligne sur une dimension automatique avec un chargement viewdid:

    override func viewDidLoad() {
    super.viewDidLoad()

    tableView.estimatedRowHeight = 70
    tableView.rowHeight = UITableViewAutomaticDimension
}

Pour résoudre ce problème de chargement initial, appliquez la méthode layoutIfNeeded avec dans une cellule de vue de tableau personnalisée:

class CustomTableViewCell: UITableViewCell {

override func awakeFromNib() {
    super.awakeFromNib()
    self.layoutIfNeeded()
    // Initialization code
}
}
bikram sapkota
la source
Il est important de définir estimatedRowHeightune valeur> 0 et non à UITableViewAutomaticDimension(qui est -1), sinon la hauteur de ligne automatique ne fonctionnera pas.
PJ_Finnegan
5

J'ai essayé la plupart des réponses à cette question et je n'ai pu en faire fonctionner aucune. La seule solution fonctionnelle que j'ai trouvée était d'ajouter ce qui suit à ma UITableViewControllersous - classe:

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

L' UIView.performWithoutAnimationappel est requis, sinon vous verrez l'animation de vue de table normale pendant le chargement du contrôleur de vue.

Chris Vig
la source
fonctionne et certainement mieux que d'invoquer reloadData deux fois
Jimmy George Thomas
2
Magie noire. Le faire viewWillAppearn'a pas fonctionné pour moi, mais le faire viewDidAppeara fonctionné.
Martin
Cette solution "a" fonctionné pour moi lorsqu'elle a été mise en œuvre dans viewDidAppear. Il n'est en aucun cas optimal pour l'expérience utilisateur car le contenu de la table saute lorsque le dimensionnement correct se produit.
richardpiazza
incroyable j'ai essayé beaucoup d'exemples mais échoue, vous êtes un démon
Shakeel Ahmed
Identique à @martin
AJ Hernandez
3

appeler cell.layoutIfNeeded()à l' intérieur a cellForRowAtfonctionné pour moi sur ios 10 et ios 11, mais pas sur ios 9.

pour obtenir ce travail sur ios 9 également, j'appelle cell.layoutSubviews()et il a fait l'affaire.

mnémonique23
la source
2

Capture d'écran jointe pour votre référencePour moi, aucune de ces approches n'a fonctionné, mais j'ai découvert que l'étiquette avait un Preferred Widthensemble explicite dans Interface Builder. Supprimer cela (décocher "Explicite") puis utiliser a UITableViewAutomaticDimensionfonctionné comme prévu.

Jack James
la source
2

J'ai essayé toutes les solutions de cette page, mais décocher utiliser les classes de taille, puis la vérifier à nouveau a résolu mon problème.

Edit: décocher les classes de taille cause beaucoup de problèmes sur le storyboard, j'ai donc essayé une autre solution. Je peuplaient mon avis de la table à mon avis de contrôleur viewDidLoadet viewWillAppearméthodes. Cela a résolu mon problème.

ACengiz
la source
1

J'ai un problème avec le redimensionnement de l'étiquette, donc je n'ai
besoin que de faire chatTextLabel.text = chatMessage.message chatTextLabel? .UpdateConstraints () après avoir configuré le texte

// code complet

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
la source
1

Dans mon cas, je mettais à jour dans un autre cycle. La hauteur de tableViewCell a donc été mise à jour après la définition de labelText. J'ai supprimé le bloc asynchrone.

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
la source
J'ai aussi un bloc asynchrone mais il est nécessaire? N'y a-t-il pas d'alternative?
Nazar Medeiros
1

Assurez-vous simplement de ne pas définir le texte de l'étiquette dans la méthode déléguée «willdisplaycell» de la vue tableau. Définissez le texte de l'étiquette dans la méthode déléguée 'cellForRowAtindexPath' pour le calcul dynamique de la hauteur.

De rien :)

Pranav Rivankar
la source
1

Dans mon cas, une vue de pile dans la cellule causait le problème. C'est un bug apparemment. Une fois que je l'ai supprimé, le problème a été résolu.

Guilherme Carvalho
la source
1
J'ai essayé toutes les autres solutions, seule votre solution a fonctionné pour merci de l'avoir partagée
tamtoum1987
1

Le problème est que les cellules initiales se chargent avant d'avoir une hauteur de ligne valide. La solution de contournement consiste à forcer un rechargement de table lorsque la vue apparaît.

- (void)viewDidAppear:(BOOL)animated
{
  [super viewDidAppear:animated];
  [self.tableView reloadData];
}
alicanozkara
la source
c'était de loin la meilleure solution qui m'a aidé. Il n'a pas corrigé le 100% de la cellule, mais jusqu'à 80% prenaient leur taille réelle. Merci beaucoup
TheTravloper
1

Pour iOS 12+ uniquement, à partir de 2019 ...

Un exemple continu d'incompétence bizarre occasionnelle d'Apple, où les problèmes durent littéralement des années.

Il semble que ce soit le cas

        cell.layoutIfNeeded()
        return cell

va le réparer. (Vous perdez des performances bien sûr.)

Telle est la vie avec Apple.

Fattie
la source
0

Dans mon cas, le problème avec la hauteur de la cellule se produit après le chargement de la vue de table initiale et une action de l'utilisateur a lieu (en appuyant sur un bouton dans une cellule qui a pour effet de modifier la hauteur de la cellule). J'ai été incapable de faire changer la hauteur de la cellule à moins que je ne le fasse:

[self.tableView reloadData];

J'ai essayé

[cell layoutIfNeeded];

mais cela n'a pas fonctionné.

Chris Prince
la source
0

Dans Swift 3. J'ai dû appeler self.layoutIfNeeded () chaque fois que je mettais à jour le texte de la cellule réutilisable.

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 Eugène
la source
0

Aucune des solutions ci-dessus n'a fonctionné, mais la combinaison suivante des suggestions a fonctionné.

J'ai dû ajouter ce qui suit dans viewDidLoad ().

DispatchQueue.main.async {

        self.tableView.reloadData()

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

        self.tableView.reloadData()

    }

La combinaison ci-dessus de reloadData, setNeedsLayout et layoutIfNeeded a fonctionné mais aucune autre. Cela pourrait être spécifique aux cellules du projet. Et oui, j'ai dû invoquer reloadData deux fois pour le faire fonctionner.

Définissez également les éléments suivants dans viewDidLoad

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = MyEstimatedHeight

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

cell.setNeedsLayout()
cell.layoutIfNeeded() 
Jimmy George Thomas
la source
Mon semblable reloadData/beginUpdates/endUpdates/reloadDatafonctionne également; reloadData doit être appelé une seconde fois. Pas besoin de l'envelopper async.
ray
0

J'ai rencontré ce problème et je l'ai résolu en déplaçant mon code d'initialisation de vue / étiquette de tableView(willDisplay cell:)À tableView(cellForRowAt:).

surélevé et émaillé
la source
ce qui n'est PAS la méthode recommandée pour initialiser une cellule. willDisplayaura de meilleures performances que cellForRowAt. Utilisez le dernier uniquement pour instancier la bonne cellule.
Martin
2 ans après, je lis ce vieux commentaire que j'ai écrit. C'était un mauvais conseil. Même si vous willDisplayavez de meilleures performances, il est recommandé d'initialiser l'interface utilisateur de votre cellule cellForRowAtlorsque la mise en page est automatique. En effet, la mise en page est calculée par UIKit après cellForRowAt et avant willDisplay. Donc, si la hauteur de votre cellule dépend de son contenu, initialisez le contenu de l'étiquette (ou autre) dans cellForRowAt.
Martin
-1
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{


//  call the method dynamiclabelHeightForText
}

utilisez la méthode ci-dessus qui renvoie dynamiquement la hauteur de la ligne. Et attribuez la même hauteur dynamique à l'étiquette que vous utilisez.

-(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;


}

Ce code vous aide à trouver la hauteur dynamique du texte affiché dans l'étiquette.

Ramesh Muthe
la source
En fait, Ramesh, cela ne devrait pas être nécessaire tant que les propriétés tableView.estimatedRowHeight et tableView.rowHeight = UITableViewAutomaticDimension sont définies. Assurez-vous également que la cellule personnalisée a des contraintes appropriées sur les différents widgets et le contentView.
BonanzaDriver