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


Comment utilisez-vous Auto Layout dans UITableViewCells dans une vue de tableau pour permettre au contenu et aux sous-vues de chaque cellule de déterminer la hauteur de la ligne (elle-même / automatiquement), tout en conservant une performance de défilement fluide?


1350
2017-09-11 16:48


origine


Réponses:


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

Description conceptuelle

Les deux premières étapes ci-dessous s'appliquent quelles que soient les versions iOS pour lesquelles vous développez.

1. Configurer et ajouter des contraintes

Dans ton UITableViewCell sous-classe, ajoutez des contraintes afin que les sous-vues de la cellule aient leurs bords épinglés sur les bords de la cellule contentView (le plus important pour les bords supérieurs et inférieurs). NOTE: ne pas épingler les sous-vues à la cellule elle-même; seulement à la cellule contentView! Laissez la taille intrinsèque du contenu de ces sous-vues déterminer la hauteur de la vue du contenu de la cellule vue de la table en vous assurant que résistance à la compression du contenu et contenu étreindre les contraintes dans la dimension verticale pour chaque sous-vue ne sont pas remplacées par les contraintes de priorité supérieure que vous avez ajoutées. (Huh? Cliquez ici.)

Rappelez-vous, l'idée est que les sous-vues de la cellule soient connectées verticalement à la vue du contenu de la cellule afin qu'elles puissent "exercer une pression" et que la vue de contenu se dilate pour les adapter. En utilisant une cellule d'exemple avec quelques sous-vues, voici une illustration visuelle de ce que certains  (pas tout!) de vos contraintes devrait ressembler à:

Example illustration of constraints on a table view cell.

Vous pouvez imaginer que plus de texte est ajouté à l'étiquette de corps multi-lignes dans la cellule exemple ci-dessus, il devra croître verticalement pour s'adapter au texte, ce qui forcera effectivement la cellule à croître en hauteur. (Bien sûr, vous devez obtenir les contraintes correctes pour que cela fonctionne correctement!)

Obtenir vos contraintes est certainement le la partie la plus difficile et la plus importante d'obtenir des hauteurs de cellules dynamiques en travaillant avec Auto Layout. 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, et il est beaucoup plus facile de déboguer lorsque les choses tournent mal. Ajouter des contraintes dans le code peut être tout aussi simple et significativement plus puissant que celui d'Interface Builder en utilisant des ancres de mise en page, ou l'une des API open source fantastiques disponibles sur GitHub.

  • Si vous ajoutez des contraintes dans le code, vous devriez le faire une fois depuis le updateConstraints méthode de votre sous-classe UITableViewCell. Notez que updateConstraints peut être appelé plus d'une fois, donc pour éviter d'ajouter les mêmes contraintes plus d'une fois, assurez-vous d'envelopper votre code d'ajout de contraintes dans updateConstraints dans une vérification d'une propriété booléenne tels que didSetupConstraints (que vous avez défini sur YES après avoir exécuté votre code d'ajout de contrainte une fois). D'un autre côté, si vous avez du code qui met à jour les contraintes existantes (comme ajuster le constantpropriété sur certaines contraintes), placez ceci dans updateConstraints mais en dehors du chèque pour didSetupConstraints Il peut donc s'exécuter à chaque fois que la méthode est appelée.

2. Déterminer les identificateurs uniques de réutilisation de cellules d'affichage de table

Pour chaque ensemble unique de contraintes dans la cellule, utilisez un identifiant de réutilisation de cellule unique. En d'autres termes, si vos cellules ont plus d'une disposition unique, chaque mise en page unique doit recevoir son propre identifiant de réutilisation. (Un bon indice que vous devez utiliser un nouvel identifiant de réutilisation est quand votre variante de cellule a un nombre différent de sous-vues, ou les sous-vues sont arrangées d'une manière distincte.)

Par exemple, si vous affichez un e-mail dans chaque cellule, vous pouvez avoir 4 mises en page uniques: des messages avec un sujet, des messages avec un sujet et un corps, des messages avec un sujet et une photo, et des messages avec un sujet, corps et photo attachement. Chaque mise en page a des contraintes complètement différentes requises pour l'atteindre, ainsi une fois la cellule initialisée et les contraintes ajoutées pour l'un de ces types de cellule, la cellule doit obtenir un identifiant de réutilisation unique spécifique à ce type de cellule. Cela signifie que lorsque vous supprimez une cellule pour la réutiliser, les contraintes ont déjà été ajoutées et sont prêtes à être activées pour ce type de cellule.

Notez qu'en raison des différences dans la taille du contenu intrinsèque, les cellules ayant les mêmes contraintes (type) peuvent toujours avoir des hauteurs variables! Ne confondez pas des mises en page fondamentalement différentes (différentes contraintes) avec des vues différentes calculées (résolues à 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 (c'est-à-dire utilisez le même identifiant de réutilisation), puis essayez de supprimer les anciennes contraintes et de définir de nouvelles contraintes après chaque file d'attente. Le moteur Auto Layout interne n'est pas conçu pour gérer les changements de contraintes à grande échelle, et vous verrez des problèmes de performances massifs.

Pour iOS 8 - Cellules auto-dimensionnantes

3. Activer l'estimation de la hauteur de la ligne

Pour activer les cellules de vue de table à redimensionnement automatique, vous devez définir les vues de la table   rowHeight propriété à UITableViewAutomaticDimension. Vous devez aussi   affecter une valeur à la propriété estimatedRowHeight. Dès que les deux   ces propriétés sont définies, le système utilise Auto Layout pour calculer le   la hauteur réelle de la rangée

Pomme: Travailler avec des cellules de vue de table à dimensionnement automatique

Avec iOS 8, Apple a intériorisé une grande partie du travail que vous deviez implémenter avant iOS 8. Pour permettre le fonctionnement du mécanisme de cellule auto-dimensionnable, vous devez d'abord définir rowHeight propriété sur la vue de la table à la constante UITableViewAutomaticDimension. Ensuite, vous devez simplement activer l'estimation de la hauteur de la ligne en définissant la vue de la table estimatedRowHeight propriété à 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 table 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 des rangées sera calculée. Pour déterminer la hauteur réelle de chaque rangée, la vue de table demande automatiquement à chaque cellule quelle hauteur contentViewdoit être basé sur la largeur fixe connue de la vue de contenu (basée sur la largeur de la vue de la table, moins les éléments supplémentaires comme un index de section ou une vue accessoire) et les contraintes d'agencement automatique ajoutées à la vue de contenu et aux sous-vues . Une fois que cette hauteur de cellule réelle a été déterminée, l'ancienne hauteur estimée pour la ligne est mise à jour avec la nouvelle hauteur réelle (et tous les ajustements au contentSize / contentOffset de la vue de table sont faits pour vous).

D'une manière générale, l'estimation que vous fournissez n'a pas besoin d'être très précise - elle est uniquement utilisée pour dimensionner correctement l'indicateur de défilement dans la vue de table, et la vue de table fait un bon ajustement de l'indicateur de défilement pour les estimations incorrectes faites défiler les cellules à l'écran. Vous devriez définir le estimatedRowHeight propriété sur la vue de la table (en viewDidLoad ou similaire) à une valeur constante qui est la hauteur de ligne "moyenne". Seulement si vos hauteurs de rangée ont une variabilité extrême (par exemple, diffèrent d'un ordre de grandeur) et que vous remarquez que l'indicateur de défilement «sautant» lorsque vous faites défiler devrait vous déranger. tableView:estimatedHeightForRowAtIndexPath: effectuer le calcul minimal requis pour renvoyer une estimation plus précise pour chaque ligne.

Pour la prise en charge d'iOS 7 (mise en œuvre du dimensionnement automatique des cellules)

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

Tout d'abord, instancier une instance hors écran d'une cellule de vue de table, une instance pour chaque identifiant de réutilisation, qui est utilisé strictement pour les calculs de hauteur. (Offscreen signifie que la référence de la cellule est stockée dans une propriété / ivar sur le contrôleur de vue et n'est jamais retournée tableView:cellForRowAtIndexPath: pour que la vue de table s'affiche réellement à l'écran.) Ensuite, la cellule doit être configurée avec le contenu exact (par exemple, texte, images, etc.) qu'elle tiendrait si elle devait être affichée dans la vue de table.

Ensuite, forcez la cellule à immédiatement mettre en forme ses sous-vues, puis utilisez le systemLayoutSizeFittingSize: méthode sur le UITableViewCellde contentView pour savoir quelle est la hauteur requise de la cellule. Utilisation UILayoutFittingCompressedSize pour obtenir la plus petite taille requise pour s'adapter à tout le contenu de la cellule. La hauteur peut ensuite être retournée depuis tableView:heightForRowAtIndexPath: méthode de délégué.

4. Utiliser les hauteurs de lignes estimées

Si la vue de votre table contient plus de deux douzaines de lignes, vous constaterez que la résolution automatique des contraintes de mise en page peut rapidement ralentir le thread principal lors du premier chargement de la vue tabulaire, comme tableView:heightForRowAtIndexPath: est appelé sur chaque ligne au premier chargement (afin de calculer la taille de l'indicateur de défilement).

Depuis iOS 7, vous pouvez (et devez absolument) utiliser le estimatedRowHeight propriété sur la vue de la table. Cela permet de fournir à la vue table 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 des rangées sera calculée (en appelant tableView:heightForRowAtIndexPath:), et la hauteur estimée mise à jour avec la hauteur réelle.

D'une manière générale, l'estimation que vous fournissez n'a pas besoin d'être très précise - elle est uniquement utilisée pour dimensionner correctement l'indicateur de défilement dans la vue de table, et la vue de table fait un bon ajustement de l'indicateur de défilement pour les estimations incorrectes faites défiler les cellules à l'écran. Vous devriez définir le estimatedRowHeight propriété sur la vue de la table (en viewDidLoad ou similaire) à une valeur constante qui est la hauteur de ligne "moyenne". Seulement si vos hauteurs de rangée ont une variabilité extrême (par exemple, diffèrent d'un ordre de grandeur) et que vous remarquez que l'indicateur de défilement «sautant» lorsque vous faites défiler devrait vous déranger. tableView:estimatedHeightForRowAtIndexPath: effectuer le calcul minimal requis pour renvoyer une estimation plus précise pour chaque ligne.

5. (Si nécessaire) Ajouter une mise en cache de hauteur de ligne

Si vous avez fait tout ce qui précède et que vous constatiez toujours que la performance est inacceptable lorsque vous effectuez la résolution de contraintes dans tableView:heightForRowAtIndexPath:, vous devrez malheureusement mettre en place une mise en cache pour les hauteurs de cellules. (L'approche proposée par les ingénieurs d'Apple.) L'idée générale est de laisser le moteur Auto Layout résoudre les contraintes la première fois, puis mettre en cache la hauteur calculée pour cette cellule et utiliser la valeur mise en cache pour toutes les futures demandes de hauteur. L'astuce consiste bien sûr à effacer la hauteur mise en cache d'une cellule lorsque survient un événement susceptible de modifier la taille de la cellule - principalement lorsque le contenu de cette cellule change ou lorsque d'autres événements importants se produisent (comme le fait l'utilisateur le curseur de taille de texte Type dynamique).

iOS 7 Exemple de code générique (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 complets de vues de table avec des hauteurs de ligne variables en raison de cellules de vue de table contenant du contenu dynamique dans UILabels.

Xamarin (C # /. NET)

Si vous utilisez Xamarin, consultez cette page exemple de projet mis ensemble par @KentBoogaart.


2265
2018-06-19 08:21



Pour IOS8, c'est vraiment 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 étiquettes de lignes, n'oubliez pas de définir le numberOfLines à 0.

  • Ne pas oublier label.preferredMaxLayoutWidth = CGRectGetWidth(tableView.bounds)

L'exemple complet de code est ici.


122
2018-04-05 09:23



Exemple rapide d'une hauteur variable UITableViewCell

Mise à jour pour Swift 3

La réponse de William Hu à Swift est bonne, mais cela m'aide à avoir des étapes simples mais détaillées quand j'apprends à faire quelque chose pour la première fois. L'exemple ci-dessous est mon projet de test tout en apprenant à faire un UITableView avec des hauteurs de cellules variables. Je me suis basé sur cet exemple UITableView de base pour Swift.

Le projet terminé devrait ressembler à ceci:

enter image description here

Créer un nouveau projet

Cela peut être juste une application vue unique.

Ajouter le code

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

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

Nous connecterons ce point de vente plus tard.

Ouvrez ViewController.swift et assurez-vous d'avoir 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 mise en page automatique) qui rendent la hauteur de cellule variable possible:

    tableView.estimatedRowHeight = 44.0
    tableView.rowHeight = UITableViewAutomaticDimension
    

Configurer le storyboard

Ajoutez une vue de 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 vue de table sur la vue de table. Et sur la cellule Prototype, faites glisser une étiquette. Utilisez la mise en page automatique pour épingler l'étiquette aux quatre bords de la vue du contenu de la cellule Vue de la table.

enter image description here

Note importante:

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

Autres paramètres de l'IB

Nom de classe personnalisé et identifiant

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

enter image description here

Zéro lignes pour l'étiquette

Définir le nombre de lignes à 0 dans votre Label. Cela signifie multi-lignes et permet à l'étiquette de se redimensionner en fonction de son contenu.

enter image description here 

Branchez les points de vente

  • Contrôle faites glisser depuis la vue Table dans le storyboard jusqu'au tableView variable dans le ViewController code.
  • Faites de même pour l'étiquette de votre cellule prototype myCellLabel variable dans le MyCustomCell classe.

Fini

Vous devriez être capable de lancer votre projet maintenant et d'obtenir des cellules avec des hauteurs variables.

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 futurs projets auront probablement plus d'une seule étiquette. Assurez-vous que tout est bien épinglé afin que la mise en page automatique puisse déterminer la bonne hauteur à utiliser. Vous devrez peut-être également utiliser une résistance à la compression verticale et un étreinte. Voir Cet article pour plus à ce sujet.
  • Si vous n'épinglez pas les bords avant et arrière (gauche et droite), vous devrez peut-être définir l'étiquette preferredMaxLayoutWidth afin qu'il sache quand enrouler la pellicule. Par exemple, si vous avez ajouté une contrainte Centre horizontalement à l'étiquette dans le projet ci-dessus plutôt que d'épingler les bords avant et arrière, vous devez ajouter cette ligne au tableView:cellForRowAtIndexPath méthode:

     cell.myCellLabel.preferredMaxLayoutWidth = tableView.bounds.width
    

Voir également


71
2018-06-11 15:18



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

J'ai décidé d'emballer cette solution intelligente par @smileyborg dans un UICollectionViewCell+AutoLayoutDynamicHeightCalculation Catégorie.

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

Il ne prend pas en compte la mise en cache mais convient à mes besoins en ce 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 dans iOS8, mais là c'est pour l'instant!


60
2018-02-10 19:41



Voici ma solution. Vous devez indiquer à TableView l'estimationHeight avant de charger la vue. Sinon, il ne sera pas capable de se comporter comme prévu.

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

43
2017-11-11 16:52



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 puis le systemLayoutSizeFittingSize méthode combinée avec AutoLayout activé renvoie un CGSizeZero sauf si vous déplacez toutes vos contraintes de cellule de la cellule à son contenu (comme suggéré par @TomSwift ici Comment redimensionner superview pour s'adapter à toutes les sous-vues avec autolayout?).

Pour ce faire, vous devez insérer le code suivant dans votre implémentation personnalisée UITableViewCell (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 réponse avec cela devrait fonctionner.


42
2017-12-03 02:23



Un gotcha assez important que je viens de rencontrer pour poster une réponse.

La réponse de @ smileyborg est la plupart du temps correcte. Cependant, si vous avez un code dans le layoutSubviews méthode de votre classe de cellule personnalisée, par exemple la définition de la preferredMaxLayoutWidth, alors il ne sera pas exécuté avec ce code:

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

Cela m'a confondu pendant un moment. Ensuite, je me suis rendu compte que c'est parce que ceux-ci ne déclenchent que des mises en page sur le 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 sûr que vous n'avez pas besoin d'appeler setNeedsLayout comme il devrait déjà être défini. Dans les cas où vous enregistrez une référence à une cellule, vous devriez probablement l'appeler. De toute façon, ça ne devrait rien faire de mal.

Une autre astuce si vous utilisez des sous-classes de cellules où vous définissez des choses comme preferredMaxLayoutWidth. Comme l'indique @smileyborg, "la taille de la cellule de votre vue de table n'a pas encore été fixée à la largeur de la vue de la table". C'est vrai, et des problèmes si vous faites votre travail dans votre sous-classe et pas dans le contrôleur de vue. Cependant, vous pouvez simplement définir le cadre de la cellule à ce stade en utilisant la largeur de la table:

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 se trouve que je cache cette cellule particulière pour la réutilisation, mais ce n'est pas pertinent).


22
2018-03-05 15:53