web-dev-qa-db-fra.com

Utilisation de la disposition automatique dans UITableView pour les dispositions de cellules dynamiques et les hauteurs de ligne variables

Comment utilisez-vous la disposition automatique dans UITableViewCells dans une vue sous forme de tableau pour permettre au contenu et aux sous-vues de chaque cellule de déterminer la hauteur des lignes (elle-même/automatiquement), tout en maintenant des performances de défilement régulières?

1406
smileyborg

TL; DR: Vous n'aimez pas lire? Passez directement aux exemples de projets sur GitHub:

Description conceptuelle

Les 2 premières étapes ci-dessous sont applicables quelle que soit la version iOS pour laquelle vous développez.

1. Configurer et ajouter des contraintes

Dans votre sous-classe UITableViewCell, ajoutez des contraintes pour que les bords des sous-vues de la cellule soient épinglés aux bords du contenu de la cellule (plus important encore). vers le haut ET les bords inférieurs). REMARQUE: n'épinglez pas les vues secondaires dans la cellule elle-même; contentView! de la cellule uniquement Laissez la taille de contenu intrinsèque de ces sous-vues piloter la hauteur de la vue du contenu de la cellule de la table en vous assurant que la compression du contenu Les contraintes de résistance et qui épousent le contenu dans la dimension verticale de chaque sous-vue ne sont pas remplacées par les contraintes de priorité supérieure que vous avez ajoutées. ( Hein? Cliquez ici. )

N'oubliez pas que l'idée est de connecter les sous-vues de la cellule verticalement à la vue de contenu de la cellule afin qu'elles puissent "exercer une pression" et faire en sorte que la vue du contenu soit agrandie. En utilisant un exemple de cellule avec quelques sous-vues, voici une illustration visuelle de ce que et certains (pas tous!) de vos contraintes devrait ressembler à ceci:

Example illustration of constraints on a table view cell.

Vous pouvez imaginer qu'à mesure que du texte est ajouté à l'étiquette de corps multiligne dans la cellule exemple ci-dessus, il devra être agrandi verticalement pour s'adapter au texte, ce qui forcera effectivement la cellule à croître en hauteur. (Bien entendu, vous devez maîtriser les contraintes pour que cela fonctionne correctement!)

Définir correctement vos contraintes est certainement la partie la plus difficile et la plus importante pour obtenir des hauteurs de cellules dynamiques en travaillant avec la disposition automatique. Si vous faites une erreur ici, cela pourrait empêcher tout le reste de fonctionner - alors prenez votre temps! Je vous recommande de configurer vos contraintes dans le code, car vous savez exactement quelles contraintes sont ajoutées où, et il est beaucoup plus facile de déboguer en cas de problème. L'ajout de contraintes dans le code peut s'avérer aussi simple et nettement plus puissant qu'Interface Builder utilisant des ancres de présentation ou l'une des fantastiques API open source disponibles sur GitHub.

  • Si vous ajoutez des contraintes dans le code, vous devez le faire une fois à partir de la méthode updateConstraints de votre sous-classe UITableViewCell. Notez que updateConstraints peut être appelé plus d'une fois. Par conséquent, pour éviter d'ajouter les mêmes contraintes plusieurs fois, veillez à envelopper le code d'ajout de contraintes dans updateConstraints dans la recherche d'une propriété booléenne telle que didSetupConstraints (que vous définissez sur OUI après avoir exécuté votre code d'ajout de contrainte une fois). Par contre, si vous avez un code qui met à jour les contraintes existantes (telles que le réglage de la propriété constant sur certaines contraintes), placez-le dans updateConstraints mais en dehors de la vérification de didSetupConstraints peut être exécuté à chaque appel de la méthode.

2. Déterminer des identificateurs uniques de réutilisation de cellules en vue de table

Pour chaque ensemble unique de contraintes dans la cellule, utilisez un identifiant unique de réutilisation de cellule. En d'autres termes, si vos cellules ont plus d'une disposition unique, chaque disposition unique doit recevoir son propre identifiant de réutilisation. (Il est judicieux d'utiliser un nouvel identifiant de réutilisation si votre variante de cellule comporte un nombre différent de sous-vues ou si les sous-vues sont organisées de manière distincte.)

Par exemple, si vous affichez un message électronique dans chaque cellule, vous pouvez avoir 4 présentations uniques: des messages avec uniquement un objet, des messages avec un sujet et un corps, des messages avec un objet et une pièce jointe avec photo, et des messages avec un objet, corps et pièce jointe photo. Chaque mise en page requiert des contraintes complètement différentes. Ainsi, une fois la cellule initialisée et les contraintes ajoutées pour l'un de ces types de cellules, la cellule doit obtenir un identifiant de réutilisation unique propre à ce type de cellule. Cela signifie que lorsque vous retirez une cellule de la file d'attente pour la réutiliser, les contraintes ont déjà été ajoutées et sont prêtes pour ce type de cellule.

Notez qu'en raison de différences de taille de contenu intrinsèque, les cellules ayant les mêmes contraintes (type) peuvent toujours avoir des hauteurs variables! Ne confondez pas des dispositions fondamentalement différentes (contraintes différentes) avec différents cadres de vue calculés (résolus à partir de contraintes identiques) en raison de différentes tailles de contenu.

  • N'ajoutez pas de cellules avec des ensembles de contraintes complètement différents au même pool de réutilisation (utilisez le même identifiant de réutilisation), puis essayez de supprimer les anciennes contraintes et de créer de nouvelles contraintes à partir de zéro après chaque file d'attente. Le moteur interne de mise en page automatique n'est pas conçu pour gérer les changements de contraintes à grande échelle et vous allez rencontrer des problèmes de performances énormes.

Pour iOS 8 - Cellules à taille automatique

3. Activer l'estimation de la hauteur des lignes

Pour activer les cellules de la vue de table à taille automatique, vous devez définir la propriété rowHeight de la vue de table sur UITableViewAutomaticDimension. Vous devez également affecter une valeur à la propriété ratedRowHeight. Dès que ces deux propriétés sont définies, le système utilise la disposition automatique pour calculer la hauteur réelle de la ligne.

Apple: tilisation de cellules d'affichage de la table à redimensionnement automatique

Avec iOS 8, Apple a internalisé une grande partie du travail que vous deviez auparavant mettre en œuvre avant iOS 8. Pour que le mécanisme de cellule à dimensionnement automatique fonctionne, vous devez d'abord définir le paramètre rowHeight propriété de la vue tabulaire à la constante UITableViewAutomaticDimension. Ensuite, vous devez simplement activer l'estimation de la hauteur des lignes en définissant la propriété estimatedRowHeight de la vue table sur une valeur différente de zéro, par exemple:

self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 44.0; // set to whatever your "average" cell height is

Cela permet de fournir à la vue tableau une estimation/un espace réservé temporaire pour les hauteurs de ligne des cellules qui ne sont pas encore à l'écran. Ensuite, lorsque ces cellules sont sur le point de défiler à l'écran, la hauteur réelle de la ligne est calculée. Pour déterminer la hauteur réelle de chaque ligne, la vue tabulaire demande automatiquement à chaque cellule quelle hauteur sa contentView doit être basée sur la largeur fixe connue de la vue de contenu (qui est basée sur la largeur de la vue tabulaire, moins les valeurs supplémentaires). un index de section ou une vue accessoire) et les contraintes de présentation automatique que vous avez ajoutées à la vue de contenu et aux sous-vues de la cellule. Une fois que la hauteur réelle de la cellule a été déterminée, l'ancienne hauteur estimée de la ligne est mise à jour avec la nouvelle hauteur réelle (et tout ajustement de la propriété contentSize/contentOffset de la vue tableau est effectué selon vos besoins).

En règle générale, l'estimation que vous fournissez ne doit pas nécessairement être très précise. Elle sert uniquement à dimensionner correctement l'indicateur de défilement dans la vue tabulaire. Cette dernière permet également d'ajuster l'indicateur de défilement lorsque les estimations sont incorrectes. faire défiler les cellules à l'écran. Vous devez définir la propriété estimatedRowHeight dans la vue tabulaire (dans viewDidLoad ou similaire) sur une valeur constante correspondant à la hauteur de ligne "moyenne". Seulement si la hauteur de vos rangées est extrêmement variable (par exemple, si vous la différenciez d'un ordre de grandeur) et si vous remarquez l'indicateur de défilement "sauter" lorsque vous faites défiler si vous vous donnez la peine d'implémenter tableView:estimatedHeightForRowAtIndexPath: pour effectuer le calcul minimal. nécessaire pour renvoyer une estimation plus précise pour chaque ligne.

Pour le support iOS 7 (implémentation de la taille de cellule automatique)

3. Faites une passe de mise en page et obtenez la hauteur de la cellule

Commencez par instancier une instance hors écran d'une cellule de vue tableau, , une instance pour chaque identificateur de réutilisation , utilisée strictement pour les calculs de hauteur. (Offscreen signifie que la référence de cellule est stockée dans une propriété/ivar sur le contrôleur de vue et n’est jamais renvoyée de tableView:cellForRowAtIndexPath: pour que la vue de table soit rendue à l’écran.) Ensuite, la cellule doit être configurée avec le contenu exact (par exemple, le texte). , images, etc.) qu’il conserverait s’il était affiché dans la vue tableau.

Ensuite, forcez la cellule à immédiatement mettre en forme ses sous-vues, puis utilisez la méthode systemLayoutSizeFittingSize: sur les UITableViewCell's contentView pour connaître la hauteur requise de la cellule. Utilisez UILayoutFittingCompressedSize pour obtenir la plus petite taille requise pour tout le contenu de la cellule. La hauteur peut alors être renvoyée à partir de la méthode tableView:heightForRowAtIndexPath: delegate.

4. Utiliser les hauteurs de rangée estimées

Si votre vue tableau contient plus d'une vingtaine de lignes, vous constaterez que la résolution des contraintes de mise en forme automatique peut rapidement alourdir le thread principal lors du premier chargement de la vue tableau, car tableView:heightForRowAtIndexPath: est appelé à chaque fois. rangée lors du premier chargement (afin de calculer la taille de l'indicateur de défilement).

Depuis iOS 7, vous pouvez (et devez absolument) utiliser la propriété estimatedRowHeight dans la vue Table. Cela permet de fournir à la vue tableau une estimation/un espace réservé temporaire pour les hauteurs de ligne des cellules qui ne sont pas encore à l'écran. Ensuite, lorsque ces cellules sont sur le point de défiler à l'écran, la hauteur réelle de la ligne est calculée (en appelant tableView:heightForRowAtIndexPath:) et la hauteur estimée est mise à jour avec la taille réelle.

En règle générale, l'estimation que vous fournissez ne doit pas nécessairement être très précise. Elle sert uniquement à dimensionner correctement l'indicateur de défilement dans la vue tabulaire. Cette dernière permet également d'ajuster l'indicateur de défilement lorsque les estimations sont incorrectes. faire défiler les cellules à l'écran. Vous devez définir la propriété estimatedRowHeight dans la vue tabulaire (dans viewDidLoad ou similaire) sur une valeur constante correspondant à la hauteur de ligne "moyenne". Seulement si la hauteur de vos rangées est extrêmement variable (par exemple, si vous la différenciez d'un ordre de grandeur) et si vous remarquez l'indicateur de défilement "sauter" lorsque vous faites défiler si vous vous donnez la peine d'implémenter tableView:estimatedHeightForRowAtIndexPath: pour effectuer le calcul minimal. nécessaire pour renvoyer une estimation plus précise pour chaque ligne.

5. (Si nécessaire) Ajouter une hauteur de rangée à la mise en cache

Si vous avez fait tout ce qui est décrit ci-dessus et constatez toujours que les performances sont trop lentes lorsque vous résolvez la contrainte dans tableView:heightForRowAtIndexPath:, vous devrez malheureusement implémenter une certaine mise en cache pour les hauteurs de cellules. (C'est l'approche proposée par les ingénieurs d'Apple.) L'idée générale est de laisser le moteur de présentation automatique résoudre les contraintes la première fois, puis de mettre en cache la hauteur calculée pour cette cellule et d'utiliser la valeur mise en cache pour toutes les demandes futures relatives à la hauteur de cette cellule. Bien entendu, l’astuce consiste à vider la cellule de la hauteur mise en cache lorsque tout ce qui pourrait modifier la hauteur de la cellule - en particulier lorsque le contenu de cette cellule est modifié ou lorsque d’autres événements importants se produisent (par exemple, l’utilisateur le curseur de taille de texte de type dynamique).

exemple de code générique iOS 7 (avec beaucoup de commentaires juteux)

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Determine which reuse identifier should be used for the cell at this 
    // index path, depending on the particular layout required (you may have
    // just one, or may have many).
    NSString *reuseIdentifier = ...;

    // Dequeue a cell for the reuse identifier.
    // Note that this method will init and return a new cell if there isn't
    // one available in the reuse pool, so either way after this line of 
    // code you will have a cell with the correct constraints ready to go.
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier];

    // Configure the cell with content for the given indexPath, for example:
    // cell.textLabel.text = someTextForThisCell;
    // ...

    // Make sure the constraints have been set up for this cell, since it 
    // may have just been created from scratch. Use the following lines, 
    // assuming you are setting up constraints from within the cell's 
    // updateConstraints method:
    [cell setNeedsUpdateConstraints];
    [cell updateConstraintsIfNeeded];

    // If you are using multi-line UILabels, don't forget that the 
    // preferredMaxLayoutWidth needs to be set correctly. Do it at this 
    // point if you are NOT doing it within the UITableViewCell subclass 
    // -[layoutSubviews] method. For example: 
    // cell.multiLineLabel.preferredMaxLayoutWidth = CGRectGetWidth(tableView.bounds);

    return cell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Determine which reuse identifier should be used for the cell at this 
    // index path.
    NSString *reuseIdentifier = ...;

    // Use a dictionary of offscreen cells to get a cell for the reuse 
    // identifier, creating a cell and storing it in the dictionary if one 
    // hasn't already been added for the reuse identifier. WARNING: Don't 
    // call the table view's dequeueReusableCellWithIdentifier: method here 
    // because this will result in a memory leak as the cell is created but 
    // never returned from the tableView:cellForRowAtIndexPath: method!
    UITableViewCell *cell = [self.offscreenCells objectForKey:reuseIdentifier];
    if (!cell) {
        cell = [[YourTableViewCellClass alloc] init];
        [self.offscreenCells setObject:cell forKey:reuseIdentifier];
    }

    // Configure the cell with content for the given indexPath, for example:
    // cell.textLabel.text = someTextForThisCell;
    // ...

    // Make sure the constraints have been set up for this cell, since it 
    // may have just been created from scratch. Use the following lines, 
    // assuming you are setting up constraints from within the cell's 
    // updateConstraints method:
    [cell setNeedsUpdateConstraints];
    [cell updateConstraintsIfNeeded];

    // Set the width of the cell to match the width of the table view. This
    // is important so that we'll get the correct cell height for different
    // table view widths if the cell's height depends on its width (due to 
    // multi-line UILabels Word wrapping, etc). We don't need to do this 
    // above in -[tableView:cellForRowAtIndexPath] because it happens 
    // automatically when the cell is used in the table view. Also note, 
    // the final width of the cell may not be the width of the table view in
    // some cases, for example when a section index is displayed along 
    // the right side of the table view. You must account for the reduced 
    // cell width.
    cell.bounds = CGRectMake(0.0, 0.0, CGRectGetWidth(tableView.bounds), CGRectGetHeight(cell.bounds));

    // Do the layout pass on the cell, which will calculate the frames for 
    // all the views based on the constraints. (Note that you must set the 
    // preferredMaxLayoutWidth on multi-line UILabels inside the 
    // -[layoutSubviews] method of the UITableViewCell subclass, or do it 
    // manually at this point before the below 2 lines!)
    [cell setNeedsLayout];
    [cell layoutIfNeeded];

    // Get the actual height required for the cell's contentView
    CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;

    // Add an extra point to the height to account for the cell separator, 
    // which is added between the bottom of the cell's contentView and the 
    // bottom of the table view cell.
    height += 1.0;

    return height;
}

// NOTE: Set the table view's estimatedRowHeight property instead of 
// implementing the below method, UNLESS you have extreme variability in 
// your row heights and you notice the scroll indicator "jumping" 
// as you scroll.
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Do the minimal calculations required to be able to return an 
    // estimated row height that's within an order of magnitude of the 
    // actual height. For example:
    if ([self isTallCellAtIndexPath:indexPath]) {
        return 350.0;
    } else {
        return 40.0;
    }
}

Exemples de projets

Ces projets sont des exemples fonctionnels de vues de tableau avec des hauteurs de ligne variables en raison des cellules de vue de tableau contenant un contenu dynamique dans les étiquettes UIL.

Xamarin (C # /. NET)

Si vous utilisez Xamarin, regardez ceci exemple de projet assemblé par @ KentBoogaart .

2348
smileyborg

Pour IOS8, c'est très simple:

override func viewDidLoad() {  
    super.viewDidLoad()

    self.tableView.estimatedRowHeight = 80
    self.tableView.rowHeight = UITableViewAutomaticDimension
}

OU 

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return UITableViewAutomaticDimension
}

Mais pour IOS7, la clé est de calculer la hauteur après autolayout,

func calculateHeightForConfiguredSizingCell(cell: GSTableViewCell) -> CGFloat {
    cell.setNeedsLayout()
    cell.layoutIfNeeded()
    let height = cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingExpandedSize).height + 1.0
    return height
}

Important 

  • Si plusieurs lignes sont étiquetées, n'oubliez pas de définir numberOfLines sur 0.

  • N'oubliez pas label.preferredMaxLayoutWidth = CGRectGetWidth(tableView.bounds)

L'exemple de code complet est ici .

EDIT Swift 4.2 UITableViewAutomaticDimension remplacé par UITableView.automaticDimension

144
William Hu

Exemple rapide d'une hauteur variable UITableViewCell

_ {Mis à jour pour Swift 3} _

La réponse de William Hu à Swift est bonne, mais elle m'aide à suivre quelques étapes simples mais détaillées pour apprendre à faire quelque chose pour la première fois. L'exemple ci-dessous est mon projet test tout en apprenant à créer une variable UITableView avec des hauteurs de cellules variables. Je me suis basé sur cet exemple UITableView de base pour Swift .

Le projet fini devrait ressembler à ceci:

 enter image description here

Créer un nouveau projet

Il peut s'agir simplement d'une application à vue unique.

Ajouter le code

Ajoutez un nouveau fichier Swift à votre projet. Nommez-le MyCustomCell. Cette classe contient les points de vente des vues que vous ajoutez à votre cellule dans le storyboard. Dans cet exemple de base, nous n’avons qu’une seule étiquette dans chaque cellule.

import UIKit
class MyCustomCell: UITableViewCell {
    @IBOutlet weak var myCellLabel: UILabel!
}

Nous connecterons cette prise plus tard.

Ouvrez ViewController.Swift et assurez-vous que vous avez le contenu suivant:

import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    // These strings will be the data for the table view cells
    let animals: [String] = [
        "Ten horses:  horse horse horse horse horse horse horse horse horse horse ",
        "Three cows:  cow, cow, cow",
        "One camel:  camel",
        "Ninety-nine sheep:  sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep baaaa sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep",
        "Thirty goats:  goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat "]

    // Don't forget to enter this in IB also
    let cellReuseIdentifier = "cell"

    @IBOutlet var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // delegate and data source
        tableView.delegate = self
        tableView.dataSource = self

        // Along with auto layout, these are the keys for enabling variable cell height
        tableView.estimatedRowHeight = 44.0
        tableView.rowHeight = UITableViewAutomaticDimension
    }

    // number of rows in table view
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.animals.count
    }

    // create a cell for each table view row
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell:MyCustomCell = self.tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as! MyCustomCell
        cell.myCellLabel.text = self.animals[indexPath.row]
        return cell
    }

    // method to run when table view cell is tapped
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("You tapped cell number \(indexPath.row).")
    }
}

Note importante:

  • Ce sont les deux lignes de code suivantes (avec la disposition automatique) qui rendent possible la hauteur de cellule variable:

    tableView.estimatedRowHeight = 44.0
    tableView.rowHeight = UITableViewAutomaticDimension
    

Installer le storyboard

Ajoutez une vue Table à votre contrôleur de vue et utilisez la disposition automatique pour l'épingler aux quatre côtés. Faites ensuite glisser une cellule de la vue tableau dans la vue tableau. Et sur la cellule Prototype, faites glisser une étiquette. Utilisez la mise en page automatique pour épingler l'étiquette sur les quatre bords de la vue Contenu de la cellule de la vue Tableau.

 enter image description here

Note importante:

  • La mise en page automatique fonctionne avec les deux lignes de code importantes que j'ai mentionnées ci-dessus. Si vous n'utilisez pas la mise en page automatique, cela ne fonctionnera pas.

Autres paramètres IB

Nom et identifiant de la classe personnalisée

Sélectionnez la cellule d'affichage de la table et définissez la classe personnalisée sur MyCustomCell (le nom de la classe dans le fichier Swift que nous avons ajouté). Définissez également l'identificateur sur cell (la même chaîne que celle que nous avons utilisée pour cellReuseIdentifier dans le code ci-dessus.

 enter image description here

Zero Lines for Label

Définissez le nombre de lignes sur 0 dans votre étiquette. Cela signifie plusieurs lignes et permet à l'étiquette de se redimensionner en fonction de son contenu.

 enter image description here

Branchez les prises

  • Faites glisser le contrôle de la vue Table du storyboard vers la variable tableView dans le code ViewController
  • Faites de même pour l’étiquette de votre cellule Prototype avec la variable myCellLabel de la classe MyCustomCell.

Fini

Vous devriez pouvoir exécuter votre projet maintenant et obtenir des cellules de hauteur variable.

Remarques

  • Cet exemple ne fonctionne que pour iOS 8 et après. Si vous devez toujours prendre en charge iOS 7, cela ne fonctionnera pas pour vous.
  • Vos propres cellules personnalisées dans vos projets futurs auront probablement plus d'une étiquette. Assurez-vous que tout est bien épinglé afin que la mise en page automatique puisse déterminer la hauteur correcte à utiliser. Vous devrez peut-être également utiliser la résistance à la compression verticale et les étreintes. Voir cet article pour plus d'informations à ce sujet.
  • Si vous n'épinglez pas les bords avant et arrière (gauche et droit), vous devrez peut-être également définir la variable preferredMaxLayoutWidth de l'étiquette de sorte qu'elle sache quand le retour à la ligne est terminé. Par exemple, si vous aviez ajouté une contrainte de centrer horizontalement à l'étiquette dans le projet ci-dessus plutôt que d'épingler les arêtes avant et arrière, vous devrez alors ajouter cette ligne à la méthode tableView:cellForRowAtIndexPath:

     cell.myCellLabel.preferredMaxLayoutWidth = tableView.bounds.width
    

Voir également

82
Suragch

J'ai enveloppé la solution iOS7 de smileyborg dans une catégorie

J'ai décidé de placer cette solution intelligente de @smileyborg dans une catégorie UICollectionViewCell+AutoLayoutDynamicHeightCalculation.

La catégorie corrige également les problèmes décrits dans la réponse de @ wildmonkey (chargement d'une cellule à partir d'une nib et systemLayoutSizeFittingSize: retournant CGRectZero).

Il ne prend en compte aucune mise en cache, mais répond à mes besoins pour le moment. N'hésitez pas à copier, coller et pirater.

UICollectionViewCell + AutoLayoutDynamicHeightCalculation.h

#import <UIKit/UIKit.h>

typedef void (^UICollectionViewCellAutoLayoutRenderBlock)(void);

/**
 *  A category on UICollectionViewCell to aid calculating dynamic heights based on AutoLayout contraints.
 *
 *  Many thanks to @smileyborg and @wildmonkey
 *
 *  @see stackoverflow.com/questions/18746929/using-auto-layout-in-uitableview-for-dynamic-cell-layouts-variable-row-heights
 */
@interface UICollectionViewCell (AutoLayoutDynamicHeightCalculation)

/**
 *  Grab an instance of the receiving type to use in order to calculate AutoLayout contraint driven dynamic height. The method pulls the cell from a nib file and moves any Interface Builder defined contrainsts to the content view.
 *
 *  @param name Name of the nib file.
 *
 *  @return collection view cell for using to calculate content based height
 */
+ (instancetype)heightCalculationCellFromNibWithName:(NSString *)name;

/**
 *  Returns the height of the receiver after rendering with your model data and applying an AutoLayout pass
 *
 *  @param block Render the model data to your UI elements in this block
 *
 *  @return Calculated constraint derived height
 */
- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block collectionViewWidth:(CGFloat)width;

/**
 *  Directly calls `heightAfterAutoLayoutPassAndRenderingWithBlock:collectionViewWidth` assuming a collection view width spanning the [UIScreen mainScreen] bounds
 */
- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block;

@end

UICollectionViewCell + AutoLayoutDynamicHeightCalculation.m

#import "UICollectionViewCell+AutoLayout.h"

@implementation UICollectionViewCell (AutoLayout)

#pragma mark Dummy Cell Generator

+ (instancetype)heightCalculationCellFromNibWithName:(NSString *)name
{
    UICollectionViewCell *heightCalculationCell = [[[NSBundle mainBundle] loadNibNamed:name owner:self options:nil] lastObject];
    [heightCalculationCell moveInterfaceBuilderLayoutConstraintsToContentView];
    return heightCalculationCell;
}

#pragma mark Moving Constraints

- (void)moveInterfaceBuilderLayoutConstraintsToContentView
{
    [self.constraints enumerateObjectsUsingBlock:^(NSLayoutConstraint *constraint, NSUInteger idx, BOOL *stop) {
        [self removeConstraint:constraint];
        id firstItem = constraint.firstItem == self ? self.contentView : constraint.firstItem;
        id secondItem = constraint.secondItem == self ? self.contentView : constraint.secondItem;
        [self.contentView addConstraint:[NSLayoutConstraint constraintWithItem:firstItem
                                                                     attribute:constraint.firstAttribute
                                                                     relatedBy:constraint.relation
                                                                        toItem:secondItem
                                                                     attribute:constraint.secondAttribute
                                                                    multiplier:constraint.multiplier
                                                                      constant:constraint.constant]];
    }];
}

#pragma mark Height

- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block
{
    return [self heightAfterAutoLayoutPassAndRenderingWithBlock:block
                                            collectionViewWidth:CGRectGetWidth([[UIScreen mainScreen] bounds])];
}

- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block collectionViewWidth:(CGFloat)width
{
    NSParameterAssert(block);

    block();

    [self setNeedsUpdateConstraints];
    [self updateConstraintsIfNeeded];

    self.bounds = CGRectMake(0.0f, 0.0f, width, CGRectGetHeight(self.bounds));

    [self setNeedsLayout];
    [self layoutIfNeeded];

    CGSize calculatedSize = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];

    return calculatedSize.height;

}

@end

Exemple d'utilisation:

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    MYSweetCell *cell = [MYSweetCell heightCalculationCellFromNibWithName:NSStringFromClass([MYSweetCell class])];
    CGFloat height = [cell heightAfterAutoLayoutPassAndRenderingWithBlock:^{
        [(id<MYSweetCellRenderProtocol>)cell renderWithModel:someModel];
    }];
    return CGSizeMake(CGRectGetWidth(self.collectionView.bounds), height);
}

Heureusement, nous n'aurons pas à faire ce jazz sur iOS8, mais le voilà pour l'instant!

62
Adam Waite

Voici ma solution. Vous devez indiquer à la TableView la hauteur estimée avant de charger la vue. Sinon, il ne pourra pas se comporter comme prévu.

Objectif c

- (void)viewWillAppear:(BOOL)animated {
    _messageField.delegate = self;
    _tableView.estimatedRowHeight = 65.0;
    _tableView.rowHeight = UITableViewAutomaticDimension;
}

Mise à jour de Swift 4.2

override func viewWillAppear(_ animated: Bool) {
    tableView.rowHeight = UITableView.automaticDimension
    tableView.estimatedRowHeight = 65.0
}
48
m1009ct0

La solution proposée par @smileyborg est presque parfaite. Si vous avez une cellule personnalisée et que vous voulez une ou plusieurs UILabel avec des hauteurs dynamiques, la méthode systemLayoutSizeFittingSize associée à AutoLayout activé renvoie une CGSizeZero à moins que vous ne déplaciez toutes vos contraintes de cellule de la cellule à sa contentView (comme suggéré par @TomSwift here Comment redimensionner les super vues pour s’adapter à toutes les sous-vues avec autolayout? ).

Pour ce faire, vous devez insérer le code suivant dans votre implémentation UITableViewCell personnalisée (grâce à @Adrian).

- (void)awakeFromNib{
    [super awakeFromNib];
    for (NSLayoutConstraint *cellConstraint in self.constraints) {
        [self removeConstraint:cellConstraint];
        id firstItem = cellConstraint.firstItem == self ? self.contentView : cellConstraint.firstItem;
        id seccondItem = cellConstraint.secondItem == self ? self.contentView : cellConstraint.secondItem;
        NSLayoutConstraint *contentViewConstraint =
        [NSLayoutConstraint constraintWithItem:firstItem
                                 attribute:cellConstraint.firstAttribute
                                 relatedBy:cellConstraint.relation
                                    toItem:seccondItem
                                 attribute:cellConstraint.secondAttribute
                                multiplier:cellConstraint.multiplier
                                  constant:cellConstraint.constant];
        [self.contentView addConstraint:contentViewConstraint];
    }
}

Mélanger @smileyborg avec cela devrait fonctionner.

44
wildmonkey

Je suis tombé sur un message assez important que je viens de rencontrer pour répondre.

La réponse de @ smileyborg est généralement correcte. Toutefois, si vous avez du code dans la méthode layoutSubviews de votre classe de cellule personnalisée, par exemple si vous définissez la variable preferredMaxLayoutWidth, il ne sera pas exécuté avec ce code:

[cell.contentView setNeedsLayout];
[cell.contentView layoutIfNeeded];

Cela m'a confondu pendant un moment. Ensuite, j'ai réalisé que c'est parce qu'ils déclenchent uniquement layoutSubviews sur la variable contentView, pas la cellule elle-même.

Mon code de travail ressemble à ceci:

TCAnswerDetailAppSummaryCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"TCAnswerDetailAppSummaryCell"];
[cell configureWithThirdPartyObject:self.app];
[cell layoutIfNeeded];
CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
return height;

Notez que si vous créez une nouvelle cellule, je suis quasiment sûr que vous n’avez pas besoin d’appeler setNeedsLayout car elle devrait déjà être définie. Dans les cas où vous sauvegardez une référence à une cellule, vous devriez probablement l'appeler. De toute façon, ça ne devrait pas faire de mal.

Un autre conseil si vous utilisez des sous-classes de cellules pour lesquelles vous définissez des éléments tels que preferredMaxLayoutWidth. Comme @smileyborg le mentionne, "la largeur de la cellule de votre vue tableau n'a pas encore été fixée à la largeur de la vue tableau". Ceci est vrai et pose des problèmes si vous effectuez votre travail dans votre sous-classe et non dans le contrôleur de vue. Cependant, vous pouvez simplement définir le cadre de la cellule à ce stade en utilisant la largeur du tableau:

Par exemple dans le calcul de la hauteur:

self.summaryCell = [self.tableView dequeueReusableCellWithIdentifier:@"TCAnswerDetailDefaultSummaryCell"];
CGRect oldFrame = self.summaryCell.frame;
self.summaryCell.frame = CGRectMake(oldFrame.Origin.x, oldFrame.Origin.y, self.tableView.frame.size.width, oldFrame.size.height);

(Il m'est arrivé de mettre cette cellule en cache pour la réutiliser, mais ce n'est pas pertinent).

24
Bob Spryn

Au cas où les gens ont toujours des problèmes avec cela. J'ai écrit un article de blog rapide sur l'utilisation de Autolayout avec UITableViews Exploitation de l'autolayout pour Dynamic Cell Heights , ainsi qu'un composant open source pour aider à rendre ceci plus abstrait et plus facile à mettre en œuvre . https: // github. com/Raizlabs/RZCellSizeManager

21
Alex Rouse

(pour Xcode 8.x/Xcode 9.x lu en bas)

Méfiez-vous du problème suivant dans Xcode 7.x, qui pourrait être source de confusion:

Interface Builder ne gère pas correctement la configuration de la cellule à dimensionnement automatique. Même si vos contraintes sont tout à fait valables, IB continuera à vous plaindre et à vous donner des suggestions confuses et des erreurs. La raison en est que IB ne souhaite pas modifier la hauteur de la ligne en fonction de vos contraintes (pour que la cellule s'adapte à votre contenu). Au lieu de cela, il garde la hauteur de la ligne fixe et commence à vous suggérer de modifier vos contraintes, que vous devez ignorer.

Par exemple, imaginons que tout soit correct, sans avertissements ni erreurs, tout fonctionne.

 enter image description here

Maintenant, si vous modifiez la taille de la police (dans cet exemple, je modifie la taille de la police de l'étiquette de description de 17,0 à 18,0).

 enter image description here

Etant donné que la taille de la police a augmenté, l'étiquette souhaite désormais occuper 3 lignes (auparavant, elle occupait 2 lignes).

Si Interface Builder fonctionnait comme prévu, la hauteur de la cellule serait redimensionnée pour s'adapter à la nouvelle hauteur de l'étiquette. Cependant, ce qui se produit réellement, c’est que IB affiche l’icône rouge d’erreur de mise en page automatique et vous suggère de modifier les priorités de calage/compression.

 enter image description here

Vous devriez ignorer ces avertissements. Au lieu de cela, vous pouvez * modifier manuellement la hauteur de la ligne (sélectionnez Cellule> Inspecteur de taille> Hauteur de ligne).

 enter image description here

Je changeais cette hauteur un clic à la fois (en utilisant le stepper) jusqu'à ce que les erreurs de la flèche rouge disparaissent! (vous obtiendrez des avertissements jaunes, à quel moment allez-vous simplement faire la mise à jour des cadres, tout devrait fonctionner).

* Notez que vous n'avez pas réellement besoin de résoudre ces erreurs rouges ou ces avertissements jaunes dans Interface Builder - au moment de l'exécution, tout fonctionnera correctement (même si IB affiche des erreurs/avertissements). Assurez-vous simplement qu'au moment de l'exécution dans le journal de la console, vous ne recevez aucune erreur AutoLayout.

En fait, essayer de toujours mettre à jour la hauteur des lignes dans IB est extrêmement ennuyeux et parfois presque impossible (à cause des valeurs fractionnaires) .

Pour éviter les avertissements/erreurs gênants de l’IB, vous pouvez sélectionner les vues concernées et dans Size Inspector pour la propriété Ambiguity choisir Verify Position Only

 enter image description here


Xcode 8.x/Xcode 9.x semble (parfois) faire les choses différemment de Xcode 7.x, mais toujours de manière incorrecte. Par exemple, même lorsque compression resistance priority/hugging priority est défini sur required (1000), Interface Builder peut étirer ou couper une étiquette pour l'adapter à la cellule (au lieu de redimensionner la hauteur de la cellule pour l'adapter à l'étiquette). Et dans un tel cas, il se peut même que les avertissements ou les erreurs AutoLayout ne s'affichent pas. Ou parfois, il fait exactement ce que Xcode 7.x a fait, décrit ci-dessus.

19

Pour définir la dimension automatique pour la hauteur de ligne et la hauteur estimée de la ligne, assurez-vous que les étapes suivantes permettent de rendre la cote automatique effective pour la présentation de hauteur de cellule/ligne. 

  • Affecter et mettre en œuvre tableview dataSource et déléguer
  • Attribuez UITableViewAutomaticDimension à rowHeight & installedRowHeight
  • Implémenter des méthodes delegate/dataSource (c'est-à-dire heightForRowAt et lui renvoyer une valeur UITableViewAutomaticDimension)

-

Objectif c:

// in ViewController.h
#import <UIKit/UIKit.h>

@interface ViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>

  @property IBOutlet UITableView * table;

@end

// in ViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];
    self.table.dataSource = self;
    self.table.delegate = self;

    self.table.rowHeight = UITableViewAutomaticDimension;
    self.table.estimatedRowHeight = UITableViewAutomaticDimension;
}

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {

    return UITableViewAutomaticDimension;
}

Rapide:

@IBOutlet weak var table: UITableView!

override func viewDidLoad() {
    super.viewDidLoad()

    // Don't forget to set dataSource and delegate for table
    table.dataSource = self
    table.delegate = self

    // Set automatic dimensions for row height
    // Swift 4.2 onwards
    table.rowHeight = UITableView.automaticDimension
    table.estimatedRowHeight = UITableView.automaticDimension


    // Swift 4.1 and below
    table.rowHeight = UITableViewAutomaticDimension
    table.estimatedRowHeight = UITableViewAutomaticDimension

}



// UITableViewAutomaticDimension calculates height of label contents/text
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    // Swift 4.2 onwards
    return UITableView.automaticDimension

    // Swift 4.1 and below
    return UITableViewAutomaticDimension
}

Pour l'instance d'étiquette dans UITableviewCell

  • Définir le nombre de lignes = 0 (& mode de saut de ligne = tronquer la queue)
  • Définissez toutes les contraintes (haut, bas, droite à gauche) par rapport à son conteneur superview/cell.
  • Facultatif: définissez la hauteur minimale de l'étiquette, si vous voulez une zone verticale minimale couverte par l'étiquette, même s'il n'y a pas de données.

enter image description here

Remarque : Si vous avez plusieurs étiquettes (UIElements) avec une longueur dynamique, ajustez-les en fonction de la taille de son contenu. priorité plus élevée.

18
Krunal

Tant que votre mise en page dans votre cellule est bonne.

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath];

    return [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
}

Mise à jour: vous devez utiliser le redimensionnement dynamique introduit dans iOS 8.

18
Chris Van Buskirk

Comme @ Bob-Spryn J'ai rencontré un casse-tête assez important pour que je poste ceci comme une réponse.

J'ai eu du mal avec @ smileyborg répondre pendant un moment. Le piège que j’ai rencontré est que si vous définissez votre cellule prototype dans IB avec des éléments supplémentaires (UILabels, UIButtons, etc.) dans IB, lorsque vous instanciez la cellule avec [[YourTableViewCellClass alloc] init], elle n’instanciera pas tous les autres éléments de cette cellule, sauf si vous avez écrit du code pour le faire. (J'ai eu une expérience similaire avec initWithStyle.)

Pour que le storyboard instancie tous les éléments supplémentaires, procurez-vous votre cellule avec [tableView dequeueReusableCellWithIdentifier:@"DoseNeeded"] (pas [tableView dequeueReusableCellWithIdentifier:forIndexPath:] car cela poserait des problèmes intéressants.) Lorsque vous faites cela, tous les éléments que vous avez définis dans IB seront instanciés.

16
nickb

Hauteur de cellule et mise en forme automatique du tableau dynamique

Un bon moyen de résoudre le problème avec la mise en page automatique du storyboard:

- (CGFloat)heightForImageCellAtIndexPath:(NSIndexPath *)indexPath {
  static RWImageCell *sizingCell = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    sizingCell = [self.tableView dequeueReusableCellWithIdentifier:RWImageCellIdentifier];
  });

  [sizingCell setNeedsLayout];
  [sizingCell layoutIfNeeded];

  CGSize size = [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
  return size.height;
}
15
Mohnasm

Une autre "solution": ignorez toutes ces frustrations et utilisez plutôt UIScrollView pour obtenir un résultat identique à UITableView.

C’était la "solution" douloureuse pour moi, après avoir mis littéralement au moins 20 heures très frustrantes à essayer de construire quelque chose comme ce que suggérait smileyborg, échouant au fil de nombreux mois et de trois versions des versions de l’App Store.

À mon avis, si vous avez vraiment besoin de la prise en charge d’iOS 7 (pour nous, c’est essentiel), la technologie est tout simplement trop fragile et vous allez vous arracher les cheveux. Et UITableView est en général excessif, sauf si vous utilisez certaines des fonctionnalités avancées d’édition de lignes et/ou si vous avez réellement besoin de prendre en charge plus de 1 000 "lignes" (dans notre application, ce n’est en réalité jamais plus de 20 lignes). 

L'avantage supplémentaire est que le code devient incroyablement simple par rapport à toutes les conneries des délégués et à celles qui sont fournies avec UITableView. Dans viewOnLoad, c'est une seule boucle de code qui a l'air élégant et qui est facile à gérer.

Voici quelques conseils sur la façon de le faire:

1) À l'aide de Storyboard ou d'un fichier nib, créez un ViewController et la vue racine associée.

2) Faites glisser le curseur sur UIScrollView sur votre vue racine.

3) Ajoutez des contraintes de haut, de bas, de gauche et de droite à la vue de niveau supérieur afin que UIScrollView remplisse la vue racine complète.

4) Ajoutez un UIView dans UIScrollView et appelez-le "conteneur". Ajoutez des contraintes en haut, en bas, à gauche et à droite à UIScrollView (son parent). KEY TRICK: Ajoutez également une contrainte "Equal widths" pour lier UIScrollView et UIView.

Vous obtiendrez une erreur "La vue de défilement a une hauteur de contenu ambiguë ambiguë" et ​​que votre conteneur UIView doit avoir une hauteur de 0 pixel. Aucune erreur ne semble avoir d'importance lorsque l'application est en cours d'exécution.

5) Créez des fichiers nib et des contrôleurs pour chacune de vos "cellules". Utilisez UIView pas UITableViewCell.

5) Dans votre ViewController racine, vous ajoutez essentiellement toutes les "lignes" au conteneur UIView et ajoutez par programme des contraintes liant leurs bords gauche et droit à la vue Conteneur, leurs bords supérieurs à la vue Conteneur supérieure (pour le premier élément) ou la cellule précédente. Ensuite, liez la dernière cellule au fond du conteneur.

Pour nous, chaque "ligne" est dans un fichier nib. Donc, le code ressemble à ceci:

class YourRootViewController {

    @IBOutlet var container: UIView! //container mentioned in step 4

    override func viewDidLoad() {

        super.viewDidLoad()

        var lastView: UIView?
        for data in yourDataSource {

            var cell = YourCellController(nibName: "YourCellNibName", bundle: nil)
            UITools.addViewToTop(container, child: cell.view, sibling: lastView)
            lastView = cell.view
            //Insert code here to populate your cell
        }

        if(lastView != nil) {
            container.addConstraint(NSLayoutConstraint(
                item: lastView!,
                attribute: NSLayoutAttribute.Bottom,
                relatedBy: NSLayoutRelation.Equal,
                toItem: container,
                attribute: NSLayoutAttribute.Bottom,
                multiplier: 1,
                constant: 0))
        }

        ///Add a refresh control, if you want - it seems to work fine in our app:
        var refreshControl = UIRefreshControl()
        container.addSubview(refreshControl!)
    }
}

Et voici le code pour UITools.addViewToTop:

class UITools {
    ///Add child to container, full width of the container and directly under sibling (or container if sibling nil):
    class func addViewToTop(container: UIView, child: UIView, sibling: UIView? = nil)
    {
        child.setTranslatesAutoresizingMaskIntoConstraints(false)
        container.addSubview(child)

        //Set left and right constraints so fills full horz width:

        container.addConstraint(NSLayoutConstraint(
            item: child,
            attribute: NSLayoutAttribute.Leading,
            relatedBy: NSLayoutRelation.Equal,
            toItem: container,
            attribute: NSLayoutAttribute.Left,
            multiplier: 1,
            constant: 0))

        container.addConstraint(NSLayoutConstraint(
            item: child,
            attribute: NSLayoutAttribute.Trailing,
            relatedBy: NSLayoutRelation.Equal,
            toItem: container,
            attribute: NSLayoutAttribute.Right,
            multiplier: 1,
            constant: 0))

        //Set vertical position from last item (or for first, from the superview):
        container.addConstraint(NSLayoutConstraint(
            item: child,
            attribute: NSLayoutAttribute.Top,
            relatedBy: NSLayoutRelation.Equal,
            toItem: sibling == nil ? container : sibling,
            attribute: sibling == nil ? NSLayoutAttribute.Top : NSLayoutAttribute.Bottom,
            multiplier: 1,
            constant: 0))
    }
}

Le seul «piège» que j’ai trouvé avec cette approche jusqu’à présent est que UITableView possède une fonctionnalité intéressante d’en-têtes de section «flottants» en haut de la vue lorsque vous faites défiler. La solution ci-dessus ne le fera que si vous ajoutez plus de programmation, mais dans notre cas particulier, cette fonctionnalité n'était pas indispensable à 100% et personne ne l'avait remarquée.

Si vous souhaitez des séparations entre vos cellules, ajoutez simplement un UIView de 1 pixel de haut au bas de votre "cellule" personnalisée qui ressemble à un diviseur.

Veillez à activer les "rebonds" et les "rebonds à la verticale" pour que le contrôle d'actualisation fonctionne et ressemble davantage à une vue de table.

TableView affiche des lignes et des séparateurs vides sous votre contenu, s'il ne remplit pas tout l'écran, contrairement à cette solution. Mais personnellement, je préfère que ces lignes vides ne soient pas présentes de toute façon - avec une hauteur de cellule variable, il m’a toujours semblé "bogué" pour avoir les lignes vides.

En espérant qu'un autre programmeur lit mon message AVANT de perdre plus de 20 heures à essayer de le comprendre avec Table View dans leur propre application. :)

13
alpsystems.com

Je devais utiliser des vues dynamiques (vues d'installation et contraintes par code) et lorsque je voulais définir la largeur de l'étiquette preferredMaxLayoutWidth, elle était 0. J'ai donc une hauteur de cellule incorrecte.

Puis j'ai ajouté 

[cell layoutSubviews];

avant d'exécuter

[cell setNeedsUpdateConstraints];
[cell updateConstraintsIfNeeded];

Après cette étiquette, la largeur était celle attendue et la hauteur dynamique calculait correctement.

11
Mansurov Ruslan
tableView.estimatedRowHeight = 343.0
tableView.rowHeight = UITableViewAutomaticDimension

 enter image description here

11
Abo3atef

Supposons que vous avez une cellule avec une sous-vue et que vous voulez que la hauteur de la cellule soit suffisamment élevée pour englober la sous-vue + le remplissage.

1) Définissez la contrainte inférieure de la sous-vue égale à cell.contentView moins le remplissage souhaité. Ne définissez pas de contraintes sur la cellule ou sur cell.contentView lui-même.

2) Définissez la propriété rowHeight ou tableView:heightForRowAtIndexPath: de la tableView sur UITableViewAutomaticDimension.

3) Définissez la propriété estimatedRowHeight ou tableView:estimatedHeightForRowAtIndexPath: de la tableView de manière à obtenir la meilleure estimation de la hauteur.

C'est tout.

8
Vadoff

Si vous faites la mise en page par programme, voici ce qu’il faut prendre en compte pour iOS 10 utilisant des ancres dans Swift.

Il y a trois règles/étapes

NUMÉRO 1: définissez ces deux propriétés de table view sur viewDidLoad, la première indique à la table qui doit attendre des tailles dynamiques pour leurs cellules, la seconde consiste simplement à laisser l'application calculer la taille de l'indicateur de barre de défilement performance.

    tableView.rowHeight = UITableViewAutomaticDimension
    tableView.estimatedRowHeight = 100

NUMÉRO 2: Il est important d'ajouter les sous-vues à la contentView de la cellule et non à la vue, mais également d'utiliser son layoutsmarginguide pour ancrer les sous-vues vers le haut et le bas. Voici un exemple concret de la procédure à suivre.

override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)
    setUpViews()
}

private func setUpViews() {

    contentView.addSubview(movieImageView)
    contentView.addSubview(descriptionLabel)
    let marginGuide = contentView.layoutMarginsGuide

    NSLayoutConstraint.activate([
        movieImageView.heightAnchor.constraint(equalToConstant: 80),
        movieImageView.widthAnchor.constraint(equalToConstant: 80),
        movieImageView.leftAnchor.constraint(equalTo: marginGuide.leftAnchor),
        movieImageView.topAnchor.constraint(equalTo: marginGuide.topAnchor, constant: 20),

        descriptionLabel.leftAnchor.constraint(equalTo: movieImageView.rightAnchor, constant: 15),
        descriptionLabel.rightAnchor.constraint(equalTo: marginGuide.rightAnchor),
        descriptionLabel.bottomAnchor.constraint(equalTo: marginGuide.bottomAnchor, constant: -15),
        descriptionLabel.topAnchor.constraint(equalTo: movieImageView.topAnchor)

        ])
}

Créez une méthode qui va ajouter les sous-vues et effectuer la mise en page, appelez-la dans la méthode init. 

NUMÉRO 3: NE PAS APPELER LA MÉTHODE:

  override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    }

Si vous le faites, vous annulerez votre implémentation.

Suivez ces 3 règles pour les cellules dynamiques dans les vues de table.

voici une implémentation fonctionnelle https://github.com/jamesrochabrun/MinimalViewController

4
James Rochabrun

Si vous avez une chaîne long. par exemple. un qui n'a pas un saut de ligne. Ensuite, vous risquez de rencontrer des problèmes.

Le correctif est mentionné par la réponse acceptée et par quelques autres réponses. Vous avez juste besoin d'ajouter

cell.myCellLabel.preferredMaxLayoutWidth = tableView.bounds.width

Je trouve la réponse de Suragh la plus complète et la plus concise, donc peu source de confusion. 

Bien que non expliquer pourquoi. Faisons cela. 

Déposez le code suivant dans un projet. 

import UIKit

class ViewController: UIViewController {

    lazy var label : UILabel = {
        let lbl = UILabel()
        lbl.translatesAutoresizingMaskIntoConstraints = false
        lbl.backgroundColor = .red
        lbl.textColor = .black
        return lbl
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        // step0: (0.0, 0.0)
        print("empty Text intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step1: (29.0, 20.5)
        label.text = "hiiiii"
        print("hiiiii intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step2: (328.0, 20.5)
        label.text = "translatesAutoresizingMaskIntoConstraints"
        print("1 translate intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step3: (992.0, 20.5)
        label.text = "translatesAutoresizingMaskIntoConstraints translatesAutoresizingMaskIntoConstraints translatesAutoresizingMaskIntoConstraints"
        print("3 translate intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step4: (328.0, 20.5)
        label.text = "translatesAutoresizingMaskIntoConstraints\ntranslatesAutoresizingMaskIntoConstraints\ntranslatesAutoresizingMaskIntoConstraints"
        print("3 translate w/ line breaks intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step5: (328.0, 61.0)
        label.numberOfLines = 0
        print("3 translate w/ line breaks and '0' numberOfLines intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step6: (98.5, 243.5)
        label.preferredMaxLayoutWidth = 100
        print("3 translate w/ line breaks | '0' numberOfLines | preferredMaxLayoutWidth: 100 intrinsicContentSize: \(label.intrinsicContentSize)")

        setupLayout()
    }
    func setupLayout(){
        view.addSubview(label)
        label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        label.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
    }
}

Notez que je n'ai ajouté aucune contrainte taille. J'ai seulement ajouté des contraintes centerX, centerY. Mais l'étiquette sera toujours correctement dimensionnée Pourquoi? 

À cause de contentSize.

Pour mieux traiter cela, conservez d'abord l'étape 0, puis commentez les étapes 1 à 6. Laissez setupLayout() rester. Observez le comportement. 

Puis décommentez step1 et observez.

Décommentez ensuite l'étape 2 et observez. 

Faites ceci jusqu'à ce que vous ayez décommenté les 6 étapes et observé leur comportement. 

Que peut conclure de tout cela? Quels facteurs peuvent changer la contenSize?

  1. Longueur du texte: Si vous avez un texte plus long, alors le largeur de votreContentSize intrinsèque augmentera
  2. Sauts de ligne: Si vous ajoutez \n, la largeur de intrinsicContentSize sera la largeur maximale de toutes les lignes. Si une ligne a 25 caractères, une autre a 2 caractères et une autre a 21 caractères, votre largeur sera calculée en fonction des 25 caractères
  3. Nombre de lignes autorisées: Vous devez définir la numberOfLines sur 0 sinon vous n'aurez pas plusieurs lignes. Votre numberOfLines ajustera votre hauteur intrinsèque deContentSize
  4. Effectuer des ajustements: Imaginez que selon votre texte, la largeur de votreContentSize intrinsèque était de 200 et de hauteur de 100, mais que vous souhaitiez limiter la largeur au conteneur de l'étiquette, qu'allez-vous faire? La solution consiste à définir la largeur souhaitée. Pour ce faire, définissez preferredMaxLayoutWidth sur 130, puis votre nouvelle taille intrinsèqueContentSize aura une largeur d'environ 130. La hauteur serait évidemment plus que 100 car vous auriez besoin de plus de lignes. Cela étant dit, si vos contraintes sont correctement définies, vous n’avez pas besoin de l’utiliser du tout! Pour plus d'informations, voir cette réponse et ses commentaires. Vous devez uniquement utiliser preferredMaxLayoutWidth si vous n’avez pas de contraintes limitant la largeur/la hauteur, comme dans l’on pourrait dire "n’enroulez pas le texte sauf s'il dépasse la preferredMaxLayoutWidth". Mais avec 100% de certitude, si vous définissez les valeurs de début/fin et numberOfLines sur 0, vous êtes bon! Longue histoire la plupart des réponses ici qui recommandent de l'utiliser sont FAUX! Tu n'en as pas besoin. En avoir besoin est un signe que vos contraintes ne sont pas définies correctement ou que vous n’avez simplement pas de contraintes

  5. Taille de la police: Notez également que si vous augmentez votre fontSize, le hauteur de intrinsicContentSize augmentera. Je n'ai pas montré ça dans mon code. Vous pouvez essayer cela vous-même.

Revenons donc à votre exemple tableViewCell: 

Tout ce que vous devez faire c'est:

  • définir la numberOfLines sur 0
  • contraindre correctement l'étiquette aux marges/bords
  • Il n'est pas nécessaire de définir preferredMaxLayoutWidth.
2
Honey

Dans mon cas, le rembourrage était dû aux hauteurs sectionHeader et sectionFooter, où le storyboard m'a permis de le modifier au minimum.

tableView.sectionHeaderHeight = 0
tableView.sectionFooterHeight = 0
1
Rashid

Je viens de faire quelques essais stupides avec les deux valeurs de rowHeight et estimatedRowHeight et je pensais juste que cela pourrait fournir des informations de débogage:

Si vous définissez les deux OR uniquement la valeur estimatedRowHeight, vous obtiendrez le comportement souhaité:

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 1.00001 // MUST be greater than 1

Nous vous suggérons de faire de votre mieux pour obtenir l'estimation correcte, mais le résultat final n'est pas différent. Cela n'affectera que votre performance.

 enter image description here


Si vous définissez uniquement rowHeight, c’est-à-dire:

tableView.rowHeight = UITableViewAutomaticDimension

votre résultat final ne serait pas comme souhaité:

 enter image description here


Si vous définissez estimatedRowHeight sur 1 ou moins, vous aurez crash sans tenir compte de rowHeight

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 1 

Je me suis écrasé avec le message d'erreur suivant:

Terminating app due to uncaught exception
'NSInternalInconsistencyException', reason: 'table view row height
must not be negative - provided height for index path (<NSIndexPath:
0xc000000000000016> {length = 2, path = 0 - 0}) is -1.000000'
    ...some other lines...

libc++abi.dylib: terminating with uncaught exception of type
NSException
1
Honey

Dans mon cas, je dois créer une cellule personnalisée avec une image provenant du serveur et pouvant avoir n'importe quelle largeur et hauteur. Et deux étiquettes UIL à taille dynamique (largeur et hauteur)

j'ai réalisé la même chose ici dans ma réponse avec autolayout et par programme:

Au-dessus de @smileyBorg, la réponse a aidé, mais systemLayoutSizeFittingSize n'a jamais fonctionné pour moi. Dans mon approche:

1. Pas d'utilisation de la propriété de calcul automatique de hauteur de ligne. 2. Pas d'utilisation de hauteur estimée. 3. Pas besoin de mise à jour inutile de contraintes de mise à jour. 4.Aucune utilisation de la largeur de disposition maximale préférée. 5. Pas d'utilisation de systemLayoutSizeFittingSize (Je devrais utiliser mais ne travaille pas pour moi, je ne sais pas ce qu’il fait en interne), mais ma méthode - (float) getViewHeight fonctionne et je sais ce qu’elle fait en interne.

Est-il possible d'avoir des hauteurs différentes dans une cellule UITableView lorsque j'utilise plusieurs façons différentes d'afficher la cellule?

1
Alok

En ce qui concerne la réponse acceptée par @smileyborg, j’ai trouvé

[cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]

être peu fiable dans certains cas où les contraintes sont ambiguës. Il est préférable de forcer le moteur de présentation à calculer la hauteur dans une direction, en utilisant la catégorie d’aide sur UIView ci-dessous:

-(CGFloat)systemLayoutHeightForWidth:(CGFloat)w{
    [self setNeedsLayout];
    [self layoutIfNeeded];
    CGSize size = [self systemLayoutSizeFittingSize:CGSizeMake(w, 1) withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
    CGFloat h = size.height;
    return h;
}

Où w: est la largeur de la tableview

1
railwayparade

Swift 4

@IBOutlet weak var tableViewHeightConstraint: NSLayoutConstraint!
@IBOutlet weak var tableView: UITableView!

redéfinit func viewDidLoad () { super.viewDidLoad ()

    self.tableView.addObserver(self, forKeyPath: "contentSize", options: [.new,.prior], context: &context)
}

// Ajout de l'observateur pour ajuster la hauteur de la table en fonction du contenu

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if context == &self.context{
        if let size = change?[NSKeyValueChangeKey.newKey] as? CGSize{
            print("-----")
            print(size.height)
            tableViewHeightConstraint.constant = size.height + 50
        }
    }
}

// Supprimer l'observateur deinit {

    NotificationCenter.default.removeObserver(self)

}
0
Manee ios

Swift 4.2

Cela peut vous aider à réduire l’écriture du même code plusieurs fois dans l’application.

J'ai ajouté l'extension ci-dessous à UITableView: 

extension UITableView {

    func setDynamicRowHeight(withEstimatedHeight height : CGFloat){
        self.estimatedRowHeight = height
        self.rowHeight = UITableViewAutomaticDimension
    }
}

et dans n'importe quelle classe j'ai besoin de définir la hauteur de ligne de la tableView dynamique

J'appelle seulement la fonction ci-dessus dans le rappel ViewDidLoad () aussi simple que cela:

  override func viewDidLoad() {
    super.viewDidLoad()
    // enter your estimated row height. 
    self.tableView.setDynamicRowHeight(withEstimatedHeight: 88.0)

}
0
MhmdRizk

Ajoutez simplement ces deux fonctions à votre contrôleur de vue, cela résoudra votre problème. Ici, la liste est un tableau de chaînes qui contient votre chaîne de chaque ligne.

 func tableView(_ tableView: UITableView, 
   estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
        tableView.rowHeight = self.calculateHeight(inString: list[indexPath.row])

    return (tableView.rowHeight) 
}

func calculateHeight(inString:String) -> CGFloat
{
    let messageString = input.text
    let attributes : [NSAttributedStringKey : Any] = [NSAttributedStringKey(rawValue: NSAttributedStringKey.font.rawValue) : UIFont.systemFont(ofSize: 15.0)]

    let attributedString : NSAttributedString = NSAttributedString(string: messageString!, attributes: attributes)

    let rect : CGRect = attributedString.boundingRect(with: CGSize(width: 222.0, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, context: nil)

    let requredSize:CGRect = rect
    return requredSize.height
}
0
Parth Barot