web-dev-qa-db-fra.com

uILabel multiligne iOS

La question suivante est en quelque sorte la suite de celle-ci:

iOS: UILabel multiligne à mise en page automatique

L'idée principale est que chaque vue est supposée indiquer sa taille (préférée) (intrinsèque) afin que AutoLayout puisse savoir comment l'afficher correctement. UILabel est juste un exemple de situation dans laquelle une vue ne peut pas à elle seule connaître la taille dont elle a besoin pour être affichée. Cela dépend de la largeur fournie.

Comme mwhuss l'a souligné, setPreferredMaxLayoutWidth a fait en sorte que l'étiquette couvre plusieurs lignes. Mais ce n’est pas la question principale ici. La question est de savoir où et quand puis-je obtenir cette largeur valeur que j’envoie comme argument à setPreferredMaxLayoutWidth.

J'ai réussi à faire quelque chose qui semble légitime, alors corrigez-moi si je me trompe de quelque façon que ce soit et dites-moi s'il vous plaît si vous connaissez un meilleur moyen. 

Dans le UIView's

-(CGSize) intrinsicContentSize

I setPreferredMaxLayoutWidth pour mes étiquettes UIL selon self.frame.width.

UIViewController

-(void) viewDidLayoutSubviews

c’est la première méthode de rappel que je connaisse où les sous-vues de la vue principale sont désignées avec leurs cadres exacts qu’ils habitent à l’écran. De l'intérieur de cette méthode, j'agis sur mes vues partielles, en invalidant leurs tailles intrinsèques de sorte que les étiquettes UIL soient divisées en plusieurs lignes en fonction de la largeur qui leur a été assignée.

59
ancajic

Il semble gênant qu'un UILabel ne définisse pas par défaut sa largeur pour la largeur de mise en page maximale préférée, si vous avez des contraintes qui définissent cette largeur sans ambiguïté. 

Dans presque tous les cas où j'ai utilisé des étiquettes sous Autolayout, la largeur de mise en page maximale préférée a été la largeur réelle de l'étiquette, une fois que le reste de ma mise en page a été effectué. 

Donc, pour que cela se produise automatiquement, j'ai utilisé une sous-classe UILabel, qui remplace setBounds:. Dans ce cas, appelez la super-implémentation. Ensuite, si ce n’est pas déjà le cas , définissez la largeur de mise en page maximale préférée sur la taille de la largeur de la limite. 

L'accent est important - la définition de la disposition maximale préférée entraîne la réalisation d'une autre passe de présentation, ce qui vous permet de vous retrouver avec une boucle infinie. 

42
jrturton

Il y a une réponse à cette question sur objc.io dans la section "Taille du contenu intrinsèque d'un texte multiligne" de Boîte à outils de mise en page automatique avancée . Voici les informations pertinentes:

La taille du contenu intrinsèque de UILabel et de NSTextField est ambiguë pour le texte multiligne. La hauteur du texte dépend de la largeur des lignes, ce qui reste à déterminer lors de la résolution des contraintes. Afin de résoudre ce problème, les deux classes ont une nouvelle propriété appelée preferredMaxLayoutWidth, qui spécifie la largeur de ligne maximale pour le calcul de la taille du contenu intrinsèque.

Comme nous ne connaissons généralement pas cette valeur à l’avance, nous devons adopter une approche en deux étapes pour parvenir à ce résultat. Tout d'abord, nous laissons Auto Layout faire son travail, puis nous utilisons le cadre obtenu dans l'étape de présentation pour mettre à jour la largeur maximale préférée et déclencher à nouveau la présentation.

Le code qu'ils donnent pour une utilisation dans un contrôleur de vue:

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    myLabel.preferredMaxLayoutWidth = myLabel.frame.size.width;
    [self.view layoutIfNeeded];
}

Jetez un coup d'œil à leur message. Vous y trouverez plus d'informations sur les raisons pour lesquelles il est nécessaire de faire la mise en page deux fois.

57
nevan king

Mettre à jour

Ma réponse originale semble être utile et je l’ai laissée intacte ci-dessous, cependant, dans mes propres projets, j’ai trouvé une solution plus fiable qui résout les bogues dans iOS 7 et iOS 8 . https: // github. com/nicksnyder/ios-cell-layout

Réponse originale

Ceci est une solution complète qui fonctionne pour moi sur iOS 7 et iOS 8

Objectif c

@implementation AutoLabel

- (void)setBounds:(CGRect)bounds {
  if (bounds.size.width != self.bounds.size.width) {
    [self setNeedsUpdateConstraints];
  }
  [super setBounds:bounds];
}

- (void)updateConstraints {
  if (self.preferredMaxLayoutWidth != self.bounds.size.width) {
    self.preferredMaxLayoutWidth = self.bounds.size.width;
  }
  [super updateConstraints];
}

@end

Rapide

import Foundation

class EPKAutoLabel: UILabel {

    override var bounds: CGRect {
        didSet {
            if (bounds.size.width != oldValue.size.width) {
                self.setNeedsUpdateConstraints();
            }
        }
    }

    override func updateConstraints() {
        if(self.preferredMaxLayoutWidth != self.bounds.size.width) {
            self.preferredMaxLayoutWidth = self.bounds.size.width
        }
        super.updateConstraints()
    }
}
32
Nick Snyder

Nous avons eu une situation dans laquelle un UILabel à mise en page automatique à l'intérieur d'un UIScrollView a été bien structuré en portrait, mais lors de la rotation en mode paysage, la hauteur de UILabel n'a pas été recalculée.

Nous avons constaté que la réponse de @jrturton corrigeait ce problème, probablement parce que maintenant, la propriété preferredMaxLayoutWidth était correctement définie.

Voici le code que nous avons utilisé. Définissez simplement la classe personnalisée du générateur d'interface sur CVFixedWidthMultiLineLabel.

CVFixedWidthMultiLineLabel.h

@interface CVFixedWidthMultiLineLabel : UILabel

@end 

CVFixedWidthMultiLineLabel.m

@implementation CVFixedWidthMultiLineLabel

// Fix for layout failure for multi-line text from
// http://stackoverflow.com/questions/17491376/ios-autolayout-multi-line-uilabel
- (void) setBounds:(CGRect)bounds {
    [super setBounds:bounds];

    if (bounds.size.width != self.preferredMaxLayoutWidth) {
        self.preferredMaxLayoutWidth = self.bounds.size.width;
    }
}

@end
9
Ben Clayton

Comme je ne suis pas autorisé à ajouter un commentaire, je suis obligé de l'ajouter comme réponse .. La version de jrturton n'a fonctionné pour moi que si j'appelle layoutIfNeeded dans updateViewConstraints avant d'obtenir le preferredMaxLayoutWidth du label en question . Sans l'appel à layoutIfNeeded, le preferredMaxLayoutWidth a toujours été 0 dans updateViewConstraints. Et pourtant, il avait toujours la valeur souhaitée lorsqu'il était coché dans setBounds:. Je n'ai pas réussi à savoir quand le correct preferredMaxLayoutWidth a été défini. Je remplace setPreferredMaxLayoutWidth: sur la sous-classe UILabel, mais il n'a jamais été appelé.

En résumé, je:

  • ... sous-classe UILabel
  • ... et remplacez setBounds: par, si ce n'est déjà fait, définissez preferredMaxLayoutWidth sur CGRectGetWidth(bounds)
  • ... appelez [super updateViewConstraints] avant le suivant
  • ... appelez layoutIfNeeded avant d'utiliser preferredMaxLayoutWidth dans le calcul de la taille de l'étiquette

EDIT: Cette solution de contournement semble seulement fonctionner, ou être nécessaire, parfois. Je viens d'avoir un problème (iOS 7/8) où la hauteur de l'étiquette n'était pas correctement calculée, car preferredMaxLayoutWidth a renvoyé 0 après l'exécution du processus de présentation. Donc, après quelques essais et erreurs (et après avoir trouvé ceci Entrée de blog ), je suis passé à nouveau à l'aide de UILabel et je n'ai défini que les contraintes de mise en page automatique supérieure, inférieure, gauche et droite. Et pour une raison quelconque, la hauteur de l'étiquette a été définie correctement après la mise à jour du texte.

2
dergab

Utilisation de boundingRectWithSize

J'ai résolu mon problème avec deux étiquettes multilignes dans un héritage UITableViewCell qui utilisait "\ n" comme saut de ligne en mesurant la largeur souhaitée comme ceci:

- (CGFloat)preferredMaxLayoutWidthForLabel:(UILabel *)label
{
    CGFloat preferredMaxLayoutWidth = 0.0f;
    NSString *text = label.text;
    UIFont *font = label.font;
    if (font != nil) {
        NSMutableParagraphStyle *mutableParagraphStyle = [[NSMutableParagraphStyle alloc] init];
        mutableParagraphStyle.lineBreakMode = NSLineBreakByWordWrapping;

        NSDictionary *attributes = @{NSFontAttributeName: font,
                                     NSParagraphStyleAttributeName: [mutableParagraphStyle copy]};
        CGRect boundingRect = [text boundingRectWithSize:CGSizeZero options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil];
        preferredMaxLayoutWidth = ceilf(boundingRect.size.width);

        NSLog(@"Preferred max layout width for %@ is %0.0f", text, preferredMaxLayoutWidth);
    }


    return preferredMaxLayoutWidth;
}

Puis appeler la méthode était alors aussi simple que:

CGFloat labelPreferredWidth = [self preferredMaxLayoutWidthForLabel:textLabel];
if (labelPreferredWidth > 0.0f) {
    textLabel.preferredMaxLayoutWidth = labelPreferredWidth;
}
[textLabel layoutIfNeeded];
2

Comme suggéré par une autre réponse, j'ai essayé de remplacer viewDidLayoutSubviews:

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    _subtitleLabel.preferredMaxLayoutWidth = self.view.bounds.size.width - 40;
    [self.view layoutIfNeeded];
}

Cela a fonctionné, mais il était visible sur l'interface utilisateur et a provoqué un "scintillement visible", c'est-à-dire que l'étiquette a d'abord été rendue avec la hauteur de deux lignes, puis qu'elle a été rendue à nouveau avec la hauteur d'une seule ligne.

Ce n'était pas acceptable pour moi.

J'ai alors trouvé une meilleure solution en surchargeant updateViewConstraints

-(void)updateViewConstraints {
    [super updateViewConstraints];

    // Multiline-Labels and Autolayout do not work well together:
    // In landscape mode the width is still "portrait" when the label determines the count of lines
    // Therefore the preferredMaxLayoutWidth must be set
    _subtitleLabel.preferredMaxLayoutWidth = self.view.bounds.size.width - 40;
}

C’était la meilleure solution pour moi, car elle ne provoquait pas le "scintillement visuel".

1
jbandi

Une solution propre consiste à définir rowcount = 0 et à utiliser une propriété pour la contrainte de hauteur de votre étiquette. Puis, une fois le contenu défini, appelez

CGSize sizeThatFitsLabel = [_subtitleLabel sizeThatFits:CGSizeMake(_subtitleLabel.frame.size.width, MAXFLOAT)];
_subtitleLabelHeightConstraint.constant = ceilf(sizeThatFitsLabel.height);

-(void) updateViewConstraints a un problème depuis iOS 7.1.

0
netshark1000

Dans iOS 8, vous pouvez résoudre les problèmes de mise en forme d'étiquettes multilignes dans une cellule en appelant cell.layoutIfNeeded() après la mise en file d'attente et la configuration de la cellule. L'appel est sans danger dans iOS 9.

Voir la réponse de Nick Snyder. Cette solution provient de son code à l’adresse https://github.com/nicksnyder/ios-cell-layout/blob/master/CellLayout/TableViewController.Swift

0
phatmann