web-dev-qa-db-fra.com

UITableView flexible / dynamic heightForRowAtIndexPath

Cas

Normalement, vous utiliseriez la méthode déléguée cellForRowAtIndexPath pour configurer votre cellule. Les informations définies pour la cellule sont importantes pour la façon dont la cellule est dessinée et sa taille.

Malheureusement, la méthode déléguée heightForRowAtIndexPath est appelée avant la méthode déléguée cellForRowAtIndexPath, nous ne pouvons donc pas simplement dire au délégué de renvoyer la hauteur de la cellule, car ce sera zéro à ce moment-là.

Nous devons donc calculer la taille avant que la cellule ne soit dessinée dans le tableau. Heureusement, il existe une méthode qui fait exactement cela, sizeWithFont, qui appartient à la classe NSString. Cependant, il y a un problème, afin de calculer dynamiquement la taille correcte, il doit savoir comment les éléments de la cellule seront présentés. Je vais le préciser dans un exemple:

Imaginez un UITableViewCell, qui contient une étiquette nommée textLabel. Dans la méthode déléguée cellForRowAtIndexPath, nous plaçons textLabel.numberOfLines = 0, qui indique essentiellement à l'étiquette qu'il peut avoir autant de lignes qu'il le faut pour présenter le texte pour une largeur spécifique. Le problème se produit si nous donnons à textLabel un texte plus grand que la largeur initialement attribuée à textLabel. La deuxième ligne apparaîtra, mais la hauteur de la cellule ne sera pas automatiquement ajustée et nous obtenons donc une vue de table à la recherche foirée.

Comme dit précédemment, nous pouvons utiliser sizeWithFont pour calculer la hauteur, mais il doit savoir quelle police est utilisée, pour quelle largeur, etc. Si, pour des raisons de simplicité, nous nous soucions simplement de la largeur, nous pourrions coder en dur que la largeur serait d'environ 320,0 (sans tenir compte du rembourrage). Mais que se passerait-il si nous utilisions UITableViewStyleGrouped au lieu de plain, la largeur serait alors d'environ 300,0 et la cellule serait à nouveau gâchée. Ou que se passe-t-il si nous passons du portrait au paysage, nous avons beaucoup plus d'espace, mais il ne sera pas utilisé depuis que nous avons codé en dur 300.0.

C'est le cas dans lequel à un moment donné, vous devez vous poser la question de savoir combien pouvez-vous éviter le codage en dur.

Mes propres pensées

Vous pouvez appeler la méthode cellForRowAtIndexPath qui appartient à la classe UITableView pour obtenir la cellule d'une certaine section et ligne. J'ai lu quelques articles qui disaient que vous ne vouliez pas faire ça, mais je ne comprends pas vraiment cela. Oui, je suis d'accord qu'il allouera déjà la cellule, mais la méthode déléguée heightForRowAtIndexPath n'est appelée que pour les cellules qui seront visibles donc la cellule sera allouée de toute façon. Si vous utilisez correctement le dequeueReusableCellWithIdentifier la cellule ne sera plus allouée dans la méthode cellForRowAtIndexPath, à la place un pointeur est utilisé et les propriétés sont juste ajustées. Alors quel est le problème?

Notez que la cellule n'est PAS dessinée dans la méthode déléguée cellForRowAtIndexPath, lorsque la cellule de vue de table devient visible, le script appellera la méthode setNeedDisplay sur UITableVieCell qui déclenche la méthode drawRect pour dessiner la cellule. Ainsi, appeler directement le délégué cellForRowAtIndexPath ne perdra pas ses performances car il doit être dessiné deux fois.

D'accord. En appelant la méthode déléguée cellForRowAtIndexPath dans la méthode déléguée heightForRowAtIndexPath, nous recevons toutes les informations dont nous avons besoin sur la cellule pour déterminer sa taille.

Vous pouvez peut-être créer votre propre méthode sizeForCell qui passe par toutes les options, que se passe-t-il si la cellule est dans le style Value1 ou Value2, etc.

Conclusion/Question

C'est juste une théorie que j'ai décrite dans mes pensées, j'aimerais savoir si ce que j'ai écrit est correct. Ou qu'il y a peut-être une autre façon d'accomplir la même chose. Notez que je veux être en mesure de faire des choses aussi flexibles que possible.

60
Mark

Oui, je suis d'accord qu'il allouera déjà la cellule, mais la méthode déléguée heightForRowAtIndexPath n'est appelée que pour les cellules qui seront visibles, de sorte que la cellule sera allouée de toute façon.

Ceci est une erreur. La vue de table doit appeler heightForRowAtIndexPath (si elle est implémentée) pour toutes les lignes qui sont dans la vue de table, pas seulement celles actuellement affichées. La raison en est qu'il doit déterminer sa hauteur totale pour afficher les bons indicateurs de défilement.

26
omz

Je le faisais par:

  1. Création d'une collection d'objets (tableau d'informations de taille (dictionnaire, NSNumber of row heights, etc.) en fonction des objets de collection qui seront utilisés pour la vue de table.

  2. Cela se fait lorsque nous traitons les données à partir d'une source locale ou distante.

  3. Je prédétermine le type et la taille de la police qui sera utilisée lors de la création de cette collection d'objets. Vous pouvez même stocker les objets UIFont ou tout autre objet personnalisé utilisé pour représenter le contenu.

  4. Ces objets de collection seront utilisés chaque fois que j'implémente les protocoles UITableViewDataSource ou UITableViewDelegate pour déterminer les tailles des instances UITableViewCell et ses sous-vues, etc.

En procédant de cette façon, vous pouvez éviter d'avoir à sous-classer UITableViewCell juste pour obtenir les différentes propriétés de taille de son contenu.

N'utilisez pas de valeur absolue pour initialiser les cadres. Utilisez une valeur relative basée sur l'orientation et les limites actuelles.

Si nous le faisons pivoter dans n'importe quelle orientation, il suffit de faire un mécanisme de redimensionnement lors de l'exécution. Assurez-vous que le masque de redimensionnement automatique est correctement défini.

Vous n'avez besoin que des hauteurs, vous n'avez pas besoin de toutes ces choses inutiles à l'intérieur d'un UITableViewCell pour déterminer la hauteur de ligne. Vous n'avez peut-être même pas besoin de la largeur, car comme je l'ai dit, la valeur de la largeur doit être relative aux limites de la vue.

8
Jesse Armand

Voici mon approche pour résoudre ce problème

  1. Je suppose dans cette solution qu'une seule étiquette a une hauteur "dynamique"
  2. Je suppose également que si nous faisons la taille automatique de l'étiquette pour étirer la hauteur à mesure que la cellule grandit, seule la hauteur de la cellule est nécessaire pour changer
  3. Je suppose que la plume a l'espacement approprié pour savoir où sera l'étiquette et combien d'espace est au-dessus et en dessous
  4. Nous ne voulons pas changer le code chaque fois que nous changeons la police ou la position de l'étiquette dans la plume

Comment mettre à jour la hauteur:

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

    // We want the UIFont to be the same as what is in the nib,
    //  but we dont want to call tableView dequeue a bunch because its slow. 
    //  If we make the font static and only load it once we can reuse it every
    //  time we get into this method
    static UIFont* dynamicTextFont;
    static CGRect textFrame;
    static CGFloat extraHeight;
    if( !dynamicTextFont ) {
        DetailCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
        dynamicTextFont = cell.resizeLabel.font;
        CGRect cellFrame = cell.frame;
        textFrame = cell.resizeLabel.frame;
        extraHeight = cellFrame.size.height-textFrame.size.height; // The space above and below the growing field
    }
    NSString* text = .... // Get this from the some object using indexPath 

    CGSize  size = [text sizeWithFont:dynamicTextFont constrainedToSize:CGSizeMake(textFrame.size.width, 200000.f) lineBreakMode:UILineBreakModeWordWrap];

    return size.height+extraHeight;
}

Problèmes:

  • Si vous n'utilisez pas de cellule prototype, vous devrez vérifier si la cellule est nulle et l'initier.
  • Votre plume/storyboard doit avoir la taille automatique UILabel et avoir plusieurs lignes définies sur 0
7
Eric

Vous devriez jeter un œil à TTTableItemCell.m dans le cadre Three20. Il suit une approche différente, essentiellement en faisant en sorte que chaque classe de cellule (avec certains paramètres prédéfinis comme la police, la disposition, etc.) implémente une méthode partagée + tableView: sizeForItem: (ou quelque chose comme ça), où le texte de l'objet item est passé. Lorsque vous recherchez le texte d'une cellule spécifique, vous pouvez également rechercher la police appropriée.

Concernant la hauteur de cellule: vous pouvez vérifier la largeur de votre tableView et, si nécessaire, soustraire les marges de UITableViewStyleGrouped et la largeur d'une éventuelle barre d'index et élément de divulgation (que vous recherchez dans le stockage de données pour les données de vos cellules ). Lorsque la largeur de la tableView change, par ex. par rotation de l'interface, vous devez appeler [tableView reloadData].

4
MrMage

Pour répondre à la question posée par l'affiche originale, qui était "est-il correct d'appeler cellForRowAtIndexPath?", Ce n'est pas le cas. Cela vous donnera une cellule mais elle ne l'allouera PAS à cet indexPath en interne ni ne sera re-mise en file d'attente (pas de méthode pour la remettre), donc vous la perdrez. Je suppose que ce sera dans un pool d'autorelease et qu'il sera finalement désalloué, mais vous créerez toujours des charges de cellules encore et encore et c'est vraiment assez de gaspillage.

Vous pouvez faire des hauteurs de cellules dynamiques, vous pouvez même les rendre assez agréables, mais cela demande beaucoup de travail pour les rendre parfaitement transparentes, encore plus si vous souhaitez prendre en charge plusieurs orientations, etc.

3
Rols

J'ai une idée de la hauteur de cellule dynamique.

Créez simplement une instance de votre cellule personnalisée en tant que variable membre de UITableViewController. Dans la méthode tableView:heightForRowAtIndexPath:, Définissez le contenu de la cellule et renvoyez la hauteur de la cellule.

De cette façon, vous ne créerez/libérerez pas de cellule plusieurs fois comme vous le feriez si vous appelez cellForRowAtIndexPath dans la méthode heightForRowAtIndexPath.

PD: Pour plus de commodité, vous pouvez également créer une méthode statique dans votre classe de cellule personnalisée qui créera une instance de cellule singleton pour le calcul de la hauteur, définira le contenu de la cellule, puis renverra sa hauteur.

Le corps de la fonction tableView:heightForRowAtIndexPath: Ressemblera maintenant à ceci:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return [MyCell cellHeightForContent:yourContent];
}
3
tonytony

Voici ma solution que j'ai utilisée pour implémenter des cellules plutôt lisses pour une application de chat.

Jusqu'à présent, j'ai toujours été vraiment très irrité par heightForCellAtIndexPath: car cela conduit à violer le principe DRY. Avec cette solution, mon heightForRowAtIndexPath: coûte 1,5 ms par cellule que je pourrais raser jusqu'à ~ 1 ms.

Fondamentalement, vous souhaitez que chaque sous-vue de votre cellule implémente sizeThatFits: créez une cellule hors écran que vous configurez, puis interrogez la vue racine avec sizeThatFits: CGSizeMake (tableViewWidth, CGFLOAT_MAX).

Il y a quelques pièges en cours de route. Certaines vues UIKit ont des opérations de définition coûteuses. Par exemple - [UITextView setText] fait beaucoup de travail. L'astuce consiste à créer une sous-classe, à tamponner la variable, puis à remplacer setNeedsDisplay pour appeler - [super setText:] lorsque la vue est sur le point d'être rendue. Bien sûr, vous devrez implémenter votre propre sizeThatFits: en utilisant les extensions UIKit.

1
lorean