web-dev-qa-db-fra.com

AutoLayout multiline UILabel coupant du texte

J'essaie d'apprendre la mise en page automatique, j'ai finalement pensé que je pouvais me débrouiller quand cela s'est produit. Je joue avec des cellules prototypes et des étiquettes. J'essaie d'avoir un 

  • Title, titleLbl - boîte bleue dans l'image
  • Money, moneyLbl - case verte dans l'image
  • Sous-titre/Description, subTitleLbl - cadre rouge dans l'image

Jusqu'ici j'ai ceci:

iphone screenshot

Ce que je veux réaliser, c'est:

  • La boîte verte doit toujours s'afficher complètement sur 1 ligne
  • La valeur à l'intérieur de la case verte doit être centrée sur la case bleue
  • La boîte bleue prend l'espace restant (-8px pour la contrainte horizontale entre le 2) et s'affiche sur plusieurs lignes si nécessaire
  • La boîte rouge doit être en dessous et afficher autant de lignes que nécessaire.

Comme vous pouvez le constater, tout fonctionne presque, à l'exception de l'exemple de la dernière ligne. J'ai essayé de faire des recherches à ce sujet et d'après ce que je peux comprendre, cela a quelque chose à voir avec la taille du contenu intrinsèque. De nombreux articles en ligne recommandent de définir setPreferredMaxLayoutWidth. Je l'ai déjà fait à plusieurs endroits pour titleLbl et cela n'a eu aucun effet. J'ai obtenu les résultats uniquement lorsque je l'ai codé en dur en 180, mais il a également ajouté des espaces/marges aux autres zones bleues situées en haut/en dessous du texte, augmentant ainsi considérablement l'espace entre les zones rouge/bleue.

Voici mes contraintes:

Titre:title constraints

Argent:money constraints

Sous-titre:subtitle constraints

D'après les tests que j'ai effectués avec d'autres exemples de texte, il semble utiliser la largeur indiquée dans le storyboard, mais toute tentative de correction ne fonctionne pas.

J'ai créé un ivar de la cellule et je l'ai utilisé pour créer la hauteur de la cellule:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    [sizingCellTitleSubMoney.titleLbl setText:[[tableViewData objectAtIndex:indexPath.row] objectForKey:@"title"]];
    [sizingCellTitleSubMoney.subtitleLbl setText:[[tableViewData objectAtIndex:indexPath.row] objectForKey:@"subtitle"]];
    [sizingCellTitleSubMoney.moneyLbl setText:[[tableViewData objectAtIndex:indexPath.row] objectForKey:@"cost"]];

    [sizingCellTitleSubMoney layoutIfNeeded];

    CGSize size = [sizingCellTitleSubMoney.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
    return size.height+1;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    MATitleSubtitleMoneyTableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifierTitleSubTitleMoney];

    if(!cell)
    {
        cell = [[MATitleSubtitleMoneyTableViewCell alloc] init];
    }


    cell.titleLbl.layer.borderColor = [UIColor blueColor].CGColor;
    cell.titleLbl.layer.borderWidth = 1;

    cell.subtitleLbl.layer.borderColor = [UIColor redColor].CGColor;
    cell.subtitleLbl.layer.borderWidth = 1;

    cell.moneyLbl.layer.borderColor = [UIColor greenColor].CGColor;
    cell.moneyLbl.layer.borderWidth = 1;


    [cell.titleLbl setText:[[tableViewData objectAtIndex:indexPath.row] objectForKey:@"title"]];
    [cell.subtitleLbl setText:[[tableViewData objectAtIndex:indexPath.row] objectForKey:@"subtitle"]];
    [cell.moneyLbl setText:[[tableViewData objectAtIndex:indexPath.row] objectForKey:@"cost"]];

    return cell;
}

J'ai essayé de définir setPreferredMaxLayoutWidth dans les sous-vues de présentation de la cellule:

- (void)layoutSubviews
{
    [super layoutSubviews];

    NSLog(@"Money lbl: %@", NSStringFromCGRect(self.moneyLbl.frame));
    NSLog(@"prefferredMaxSize: %f \n\n", self.moneyLbl.frame.Origin.x-8);

    [self.titleLbl setPreferredMaxLayoutWidth: self.moneyLbl.frame.Origin.x-8];
}

bûche:

2014-04-30 21:37:32.475 AutoLayoutTestBed[652:60b] Money lbl: {{209, 20}, {91, 61}}
2014-04-30 21:37:32.475 AutoLayoutTestBed[652:60b] prefferredMaxSize: 201.000000 

C'est ce à quoi je m'attendrais car il y a 8px entre les 2 éléments et la console le vérifie. Cela fonctionne parfaitement sur la première ligne, peu importe la quantité de texte que j'ajoute à la zone bleue, qui est identique au storyboard, mais toute augmentation de la zone verte annule le calcul de la largeur. J'ai essayé de définir cela dans le tableView:willDisplayCell et le tableView:heightForRowAtIndexPath: également en fonction des réponses à d'autres questions. Mais peu importe ce que cela ne suffit pas mise en page.

Toute aide, idées ou commentaires seraient grandement appréciés

29
Simon McLoughlin

J'ai trouvé une réponse encore meilleure après avoir lu ceci: http://johnszumski.com/blog/auto-layout-for-table-view-cells-with-dynamic-heights

créer une sous-classe UILabel pour écraser layoutSubviews comme ceci:

- (void)layoutSubviews
{
    [super layoutSubviews];

    self.preferredMaxLayoutWidth = CGRectGetWidth(self.bounds);

    [super layoutSubviews];
}

Cela garantit que la preferredMaxLayoutWidth est toujours correcte.

Mettre à jour:

Après plusieurs autres versions d'iOS et des tests de plusieurs cas d'utilisation, j'ai trouvé que ce qui précède n'était pas suffisant pour tous les cas. À partir de iOS 8 (je crois), dans certaines circonstances, seule didSetbounds a été appelée à temps avant le chargement de l'écran.

Très récemment, dans iOS 9, je rencontrais un autre problème lorsqu’on utilisait une UIVIewController modale avec une UITableView, à savoir que layoutSubviews et set bounds étaient appelés aprèsheightForRowAtIndexPath. Après beaucoup de débogage, la seule solution consistait à remplacer set frame.

Le code ci-dessous semble maintenant nécessaire pour s’assurer qu’il fonctionne sur tous les iOS, contrôleurs, tailles d’écran, etc.

override public func layoutSubviews()
{
    super.layoutSubviews()
    self.preferredMaxLayoutWidth = self.bounds.width
    super.layoutSubviews()
}

override public var bounds: CGRect
{
    didSet
    {
        self.preferredMaxLayoutWidth = self.bounds.width
    }
}

override public var frame: CGRect
{
    didSet
    {
        self.preferredMaxLayoutWidth = self.frame.width
    }
}
27

Je ne suis pas sûr de bien vous comprendre et ma solution est loin d'être la meilleure, mais la voici:

J'ai ajouté une contrainte de hauteur sur l'étiquette coupée, connectée à la sortie de la cellule. J'ai remplacé la méthode layoutSubview:

- (void) layoutSubviews {
    [super layoutSubviews];

    [self.badLabel setNeedsLayout];
    [self.badLabel layoutIfNeeded];

    self.badLabelHeightConstraint.constant = self.badLabel.intrinsicContentSize.height;
}

et alto! S'il vous plaît essayez cette méthode et dites-moi des résultats, merci.

2
Nikita Took

J'ai passé du temps à emballer la cellule, ressemblant à ceci:

+--------------------------------------------------+
| This is my cell.contentView                      |
| +------+ +-----------------+ +--------+ +------+ |
| |      | |    multiline    | | title2 | |      | |
| | img  | +-----------------+ +--------+ | img  | |
| |      | +-----------------+ +--------+ |      | |
| |      | |     title3      | | title4 | |      | |
| +------+ +-----------------+ +--------+ +------+ |
|                                                  |
+--------------------------------------------------+

Après plusieurs tentatives, je suis venu avec une solution pour utiliser une cellule prototype qui fonctionne réellement.

Le problème principal était que mes étiquettes pourraient être ou pourraient ne pas être là . Chaque élément devrait être facultatif.

Tout d’abord, notre cellule "prototype" ne doit disposer que des éléments de mise en page lorsque nous ne sommes pas dans un environnement "réel".

Par conséquent, j'ai créé l'indicateur "heightCalculationInProgress".

Lorsque quelqu'un (source de données de tableView) demande à calculer la hauteur, nous fournissons des données à la cellule prototype à l'aide du bloc: Nous demandons ensuite à notre cellule prototype de mettre en page le contenu et de la récupérer. Simple.

Afin d'éviter le bug iOS7 avec la récursion layoutSubview, il existe une variable "layoutingLock"

- (CGFloat)heightWithSetupBlock:(nonnull void (^)(__kindof UITableViewCell *_Nonnull cell))block {
    block(self);

    self.heightCalculationInProgress = YES;

    [self setNeedsLayout];
    [self layoutIfNeeded];

    self.layoutingLock = NO;
    self.heightCalculationInProgress = NO;

    CGFloat calculatedHeight = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
    CGFloat height = roundf(MAX(calculatedHeight, [[self class] defaultHeight]));

    return height;
}

Ce "bloc" de l'extérieur pourrait ressembler à ceci:

[prototypeCell heightWithSetupBlock:^(__kindof UITableViewCell * _Nonnull cell) {
    @strongify(self);
    cell.frame = CGRectMake(0, 0, CGRectGetWidth(self.view.frame), 0);
    cell.dataObject = dataObject;
    cell.multilineLabel.text = @"Long text";
    // Include any data here
}];

Et commence maintenant la partie la plus intéressante. Mise en page:

- (void)layoutSubviews {
    if(self.layoutingLock){
        return;
    }

    // We don't want to do this layouting things in 'real' cells
    if (self.heightCalculationInProgress) {

    // Because of:
    // Support for constraint-based layout (auto layout)
    // If nonzero, this is used when determining -intrinsicContentSize for multiline labels

    // We're actually resetting internal constraints here. And then we could reuse this cell with new data to layout it correctly
        _multilineLabel.preferredMaxLayoutWidth = 0.f;
        _title2Label.preferredMaxLayoutWidth = 0.f;
        _title3Label.preferredMaxLayoutWidth = 0.f;
        _title4Label.preferredMaxLayoutWidth = 0.f;
    }

    [super layoutSubviews];

    // We don't want to do this layouting things in 'real' cells
    if (self.heightCalculationInProgress) {

        // Make sure the contentView does a layout pass here so that its subviews have their frames set, which we
        // need to use to set the preferredMaxLayoutWidth below.
        [self.contentView setNeedsLayout];
        [self.contentView layoutIfNeeded];

        // Set the preferredMaxLayoutWidth of the mutli-line bodyLabel based on the evaluated width of the label's frame,
        // as this will allow the text to wrap correctly, and as a result allow the label to take on the correct height.
        _multilineLabel.preferredMaxLayoutWidth = CGRectGetWidth(_multilineLabel.frame);
        _title2Label.preferredMaxLayoutWidth = CGRectGetWidth(_title2Label.frame);
        _title3Label.preferredMaxLayoutWidth = CGRectGetWidth(title3Label.frame);
        _title4Label.preferredMaxLayoutWidth = CGRectGetWidth(_title4Label.frame);
    }

    if (self.heightCalculationInProgress) {
        self.layoutingLock = YES;
    }
}
1
Aleksei Minaev

Omg, viens d'avoir le même problème. La réponse de @Simon McLoughlin ne m'a pas aidé. Mais

titleLabel.adjustsFontSizeToFitWidth = true

fait une magie! Cela fonctionne, je ne sais pas pourquoi mais… .. Et oui, votre étiquette doit avoir une valeur explicite preferredMaxLayoutWidth.

0
Mike Demidov