web-dev-qa-db-fra.com

boundingRectWithSize for NSAttributedString renvoyant une taille incorrecte

J'essaie d'obtenir le rect pour une chaîne attribuée, mais l'appel boundingRectWithSize ne respecte pas la taille que je transmets et renvoie un rect avec une seule hauteur de ligne plutôt qu'une grande hauteur (c'est une longue chaîne). J'ai expérimenté en passant une très grande valeur pour la hauteur et aussi 0 comme dans le code ci-dessous, mais le rect retourné est toujours le même.

CGRect paragraphRect = [attributedText boundingRectWithSize:CGSizeMake(300,0.0)
  options:NSStringDrawingUsesDeviceMetrics
  context:nil];

Est-ce cassé ou dois-je faire autre chose pour le renvoyer un rect pour le texte enveloppé?

148
RunLoop

On dirait que vous ne fournissiez pas les bonnes options. Pour l’emballage des étiquettes, veuillez fournir au moins:

CGRect paragraphRect =
  [attributedText boundingRectWithSize:CGSizeMake(300.f, CGFLOAT_MAX)
  options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading)
  context:nil];

Remarque: si la largeur du texte d'origine est inférieure à 300. S'il n'y a pas de retour à la ligne, assurez-vous que la taille de la reliure est correcte, sinon vous obtiendrez toujours des résultats erronés.

303
Ed McManus

Cette méthode semble boguée à bien des égards. D'une part, comme vous le constatez, il ne respecte pas les contraintes de largeur. D'autre part, je l'ai vu planter, car il semble supposer que tous les attributs sont de type NSObject (par exemple, il a essayé de passer _isDefaultFace à un CTFontRef). Il se bloque parfois aussi lorsqu'un contexte de dessin de chaîne est fourni car il tente d'ajouter un attribut sans valeur à une chaîne attribuée mutable en coulisse.

Je vous encourage à éviter complètement cette méthode. Vous pouvez utiliser directement Core Text pour estimer la taille de la chaîne, si vous pouvez gérer le temps système nécessaire à la création d'une encapsuleuse pour chaque chaîne à dessiner. Cela ne respecte pas non plus précisément les contraintes de largeur, mais il me semble que cela ne dépasse pas quelques pixels, selon mon expérience.

CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)attrString);
CGSize targetSize = CGSizeMake(320, CGFLOAT_MAX);
CGSize fitSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, [attrString length]), NULL, targetSize, NULL);
CFRelease(framesetter);
118
warrenm

Pour une raison quelconque, boundingRectWithSize renvoie toujours une taille incorrecte. J'ai trouvé une solution. Il existe une méthode pour UItextView -sizeThatFits qui renvoie la taille appropriée pour le jeu de texte. Ainsi, au lieu d'utiliser boundingRectWithSize, créez un UITextView, avec un cadre aléatoire, et appelez-le sizeThatFits avec la largeur et la hauteur respectives CGFLOAT_MAX. Il retourne la taille qui aura la bonne hauteur.

   UITextView *view=[[UITextView alloc] initWithFrame:CGRectMake(0, 0, width, 10)];   
   view.text=text;
   CGSize size=[view sizeThatFits:CGSizeMake(width, CGFLOAT_MAX)];
   height=size.height; 

Si vous calculez la taille dans une boucle while, n'oubliez pas d'ajouter cela dans un pool autorelease, car il y aura n nombre de UITextView créés, la mémoire d'exécution de l'application augmentera si nous n'utilisons pas autoreleasepool.

45
shoan

Ed McManus a certainement fourni une clé pour que cela fonctionne. J'ai trouvé un cas qui ne fonctionne pas

UIFont *font = ...
UIColor *color = ...
NSDictionary *attributesDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
                                     font, NSFontAttributeName,
                                     color, NSForegroundColorAttributeName,
                                     nil];

NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString: someString attributes:attributesDictionary];

[string appendAttributedString: [[NSAttributedString alloc] initWithString: anotherString];

CGRect rect = [string boundingRectWithSize:constraint options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading) context:nil];

rect n'aura pas la bonne hauteur. Notez que anotherString (qui est ajouté à chaîne ) a été initialisé sans dictionnaire d'attributs. Ceci est un initialiseur légitime pour anotherString mais boundingRectWithSize: ne donne pas une taille précise dans ce cas.

31
Paul Heller

Ma décision finale après une longue enquête:
- boundingRectWithSize la fonction retourne la taille correcte pour la séquence ininterrompue de caractères seulement! Si la chaîne contient des espaces ou quelque chose d'autre (appelé par Apple "Certains des glyphes"), il est impossible d'obtenir la taille réelle du rect nécessaire pour afficher du texte!
J'ai remplacé les espaces dans mes chaînes par des lettres et obtenu immédiatement le résultat correct.

Apple dit ici: https://developer.Apple.com/documentation/foundation/nsstring/1524729-boundingrectwithssize

"Cette méthode renvoie les limites réelles des glyphes dans la chaîne. Certains des glyphes (espaces, par exemple) sont autorisés à chevaucher les contraintes de présentation spécifiées par la taille transmise. Ainsi, dans certains cas, la valeur width du composant size de le CGRect renvoyé peut dépasser la valeur de largeur du paramètre size. "

Il est donc nécessaire de trouver un autre moyen de calculer le montant réel ...


Après un long processus de recherche, la solution a finalement été trouvée !!! Je ne suis pas sûr que cela fonctionnera bien pour tous les cas liés à UITextView, mais une chose principale et importante a été détectée!

La fonction boundingRectWithSize ainsi que CTFramesetterSuggestFrameSizeWithConstraints (et de nombreuses autres méthodes) calculeront la taille et la portion de texte correctes lorsque le rectangle correct est utilisé. Par exemple - UITextView a textView.bounds.size.width - et cette valeur n'est pas le rectangle utilisé par le système lorsque le texte est dessiné sur UITextView.

J'ai trouvé un paramètre très intéressant et effectué un calcul simple dans le code:

CGFloat padding = textView.textContainer.lineFragmentPadding;  
CGFloat  actualPageWidth = textView.bounds.size.width - padding * 2;

Et la magie fonctionne - tous mes textes calculés correctement maintenant! Prendre plaisir!

version quatre rapide

let string = "A great test string."
let font = UIFont.systemFont(ofSize: 14)
let attributes: [NSAttributedStringKey: Any] = [.font: font]
let attributedString = NSAttributedString(string: string, attributes: attributes)
let largestSize = CGSize(width: bounds.width, height: .greatestFiniteMagnitude)

//Option one (best option)
let framesetter = CTFramesetterCreateWithAttributedString(attributedString)
let textSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRange(), nil, largestSize, nil)

//Option two
let textSize = (string as NSString).boundingRect(with: largestSize, options: [.usesLineFragmentOrigin , .usesFontLeading], attributes: attributes, context: nil).size

//Option three
let textSize = attributedString.boundingRect(with: largestSize, options: [.usesLineFragmentOrigin , .usesFontLeading], context: nil).size

Mesurer le texte avec CTFramesetter fonctionne mieux car il fournit des tailles entières et gère bien les emoji et autres caractères unicode.

11
David Rees

Je n’ai eu de la chance avec aucune de ces suggestions. Ma chaîne contenait des puces Unicode et je soupçonne qu’elles causaient des problèmes de calcul. J'ai remarqué que UITextView gérait bien le dessin, j'ai donc cherché à en tirer parti pour en tirer le meilleur parti. J'ai fait ce qui suit, qui n'est probablement pas aussi optimal que les méthodes de dessin de NSString, mais au moins c'est précis. Il est également légèrement plus optimal d’initialiser une UITextView que d’appeler -sizeThatFits:.

NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(width, CGFLOAT_MAX)];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[layoutManager addTextContainer:textContainer];

NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:formattedString];
[textStorage addLayoutManager:layoutManager];

const CGFloat formattedStringHeight = ceilf([layoutManager usedRectForTextContainer:textContainer].size.height);
9
Ricky

Si vous souhaitez obtenir un cadre de sélection en tronquant la queue, cette question peut vous aider.

CGFloat maxTitleWidth = 200;

NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
paragraph.lineBreakMode = NSLineBreakByTruncatingTail;

NSDictionary *attributes = @{NSFontAttributeName : self.textLabel.font,
                             NSParagraphStyleAttributeName: paragraph};

CGRect box = [self.textLabel.text
              boundingRectWithSize:CGSizeMake(maxTitleWidth, CGFLOAT_MAX)
              options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
              attributes:attributes context:nil];
9
Roman B.

Il s'avère que CHAQUE partie d'une chaîne NSAttributedString doit avoir un dictionnaire défini avec au moins NSFontAttributeName et NSForegroundColorAttributeName, si vous souhaitez que boundingRectWithSize fonctionne réellement!

Je ne vois cela documenté nulle part.

7
Ben Wheeler

@warrenm Désolé de dire que la méthode de fabrication de cadres n'a pas fonctionné pour moi.

Cette fonction peut nous aider à déterminer la taille de trame nécessaire pour une plage de chaînes d'un NSAttributedString dans un kit SDK iphone/Ipad pour une largeur donnée:

Il peut être utilisé pour une hauteur dynamique de cellules UITableView.

- (CGSize)frameSizeForAttributedString:(NSAttributedString *)attributedString
{
    CTTypesetterRef typesetter = CTTypesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);
    CGFloat width = YOUR_FIXED_WIDTH;

    CFIndex offset = 0, length;
    CGFloat y = 0;
    do {
        length = CTTypesetterSuggestLineBreak(typesetter, offset, width);
        CTLineRef line = CTTypesetterCreateLine(typesetter, CFRangeMake(offset, length));

        CGFloat ascent, descent, leading;
        CTLineGetTypographicBounds(line, &ascent, &descent, &leading);

        CFRelease(line);

        offset += length;
        y += ascent + descent + leading;
    } while (offset < [attributedString length]);

    CFRelease(typesetter);

    return CGSizeMake(width, ceil(y));
}

Merci à HADDAD AISS >>> http://haddadissa.blogspot.in/2010/09/compute-needed-heigh-for-fixed-width-of.html

7
Karan Alangat

J'ai constaté que la solution privilégiée ne gère pas les sauts de ligne.

J'ai trouvé que cette approche fonctionne dans tous les cas:

UILabel* dummyLabel = [UILabel new];
[dummyLabel setFrame:CGRectMake(0, 0, desiredWidth, CGFLOAT_MAX)];
dummyLabel.numberOfLines = 0;
[dummyLabel setLineBreakMode:NSLineBreakByWordWrapping];
dummyLabel.attributedText = myString;
[dummyLabel sizeToFit];
CGSize requiredSize = dummyLabel.frame.size;
7
dmc

J'ai eu le même problème avec le fait de ne pas obtenir une taille précise en utilisant ces techniques et j'ai changé mon approche pour que cela fonctionne.

J'ai longtemps essayé d'attribuer une chaîne à une vue de défilement afin qu'elle s'affiche correctement sans être tronquée. Ce que j'ai fait pour que le texte fonctionne de manière fiable, c'est de ne pas définir la hauteur du tout comme une contrainte, mais plutôt de laisser la taille intrinsèque prendre le dessus. Maintenant, le texte est affiché correctement sans être tronqué et je n'ai pas à calculer la hauteur.

Je suppose que si j'avais besoin d'obtenir la hauteur de manière fiable, je créerais une vue masquée ainsi que ces contraintes et obtiendrais la hauteur du cadre une fois les contraintes appliquées.

4
Brennan

Je suis un peu en retard au jeu - mais j’essaie de trouver un moyen de trouver le cadre de sélection qui tient autour d’une chaîne attribuée pour créer un anneau de focus, comme le fait l’édition d’un fichier dans le Finder. tout ce que j'avais essayé échouait lorsqu'il y avait des espaces à la fin de la chaîne ou plusieurs espaces à l'intérieur de la chaîne. boundingRectWithSize échoue lamentablement pour cela, ainsi que CTFramesetterCreateWithAttributedString.

En utilisant un NSLayoutManager, le code suivant semble faire l'affaire dans tous les cas que j'ai trouvés jusqu'à présent et renvoie un rect qui limite parfaitement la chaîne. Bonus: si vous sélectionnez le texte, les bords de la sélection vont jusqu'aux limites du recto retourné. Le code ci-dessous utilise layoutManager à partir de NSTextView.

NSLayoutManager* layout = [self layoutManager];
NSTextContainer* container = [self textContainer];

CGRect focusRingFrame = [layout boundingRectForGlyphRange:NSMakeRange(0, [[self textStorage] length]) inTextContainer:container];
3
Chris Walken
textView.textContainerInset = UIEdgeInsetsZero;
NSString *string = @"Some string";
NSDictionary *attributes = @{NSFontAttributeName:[UIFont systemFontOfSize:12.0f], NSForegroundColorAttributeName:[UIColor blackColor]};
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:string attributes:attributes];
[textView setAttributedText:attributedString];
CGRect textViewFrame = [textView.attributedText boundingRectWithSize:CGSizeMake(CGRectGetWidth(self.view.frame)-8.0f, 9999.0f) options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading) context:nil];
NSLog(@"%f", ceilf(textViewFrame.size.height));

Fonctionne parfaitement sur toutes les polices!

2
Dmitry Coolerov

Une chose que je remarquais, c'est que le rect qui reviendrait de (CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options attributes:(NSDictionary *)attributes context:(NSStringDrawingContext *)context aurait une largeur plus grande que celle que je transmettais. Lorsque cela se produirait, ma chaîne serait tronquée. Je l'ai résolu comme ça:

NSString *aLongString = ...
NSInteger width = //some width;            
UIFont *font = //your font;
CGRect rect = [aLongString boundingRectWithSize:CGSizeMake(width, CGFLOAT_MAX)
                                        options:(NSStringDrawingUsesFontLeading | NSStringDrawingUsesLineFragmentOrigin)
                                     attributes:@{ NSFontAttributeName : font,
                                                   NSForegroundColorAttributeName : [UIColor whiteColor]}
                                        context:nil];

if(rect.size.width > width)
{
    return rect.size.height + font.lineHeight;
}
return rect.size.height;

Pour un peu plus de contexte; J'avais un texte multiligne et j'essayais de trouver la bonne hauteur pour l'afficher. BoundRectWithSize renvoyait parfois une largeur supérieure à celle que je spécifierais. Ainsi, lorsque j'utilisais mon passé en largeur et la hauteur calculée pour afficher mon texte, il serait tronqué. Lors des tests, boundingRectWithSize utilisait une mauvaise largeur, mais la hauteur à laquelle la hauteur serait réduite était de 1 ligne. Je vérifiais donc si la largeur était supérieure et, le cas échéant, si la hauteur de ligne de la police était suffisante pour fournir suffisamment d'espace pour éviter les troncatures.

1
odyth

J'ai eu le même problème, mais j'ai reconnu que la hauteur limitée était correctement définie. Alors j'ai fait ce qui suit:

-(CGSize)MaxHeighForTextInRow:(NSString *)RowText width:(float)UITextviewWidth {

    CGSize constrainedSize = CGSizeMake(UITextviewWidth, CGFLOAT_MAX);

    NSDictionary *attributesDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
                                          [UIFont fontWithName:@"HelveticaNeue" size:11.0], NSFontAttributeName,
                                          nil];

    NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:RowText attributes:attributesDictionary];

    CGRect requiredHeight = [string boundingRectWithSize:constrainedSize options:NSStringDrawingUsesLineFragmentOrigin context:nil];

    if (requiredHeight.size.width > UITextviewWidth) {
        requiredHeight = CGRectMake(0, 0, UITextviewWidth, requiredHeight.size.height);
    }

    return requiredHeight.size;
}
1
Marcel
    NSDictionary *stringAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
                                      [UIFont systemFontOfSize:18], NSFontAttributeName,
                                      [UIColor blackColor], NSForegroundColorAttributeName,
                                      nil];

    NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:myLabel.text attributes:stringAttributes];
    myLabel.attributedText = attributedString; //this is the key!

    CGSize maximumLabelSize = CGSizeMake (screenRect.size.width - 40, CGFLOAT_MAX);

    CGRect newRect = [myLabel.text boundingRectWithSize:maximumLabelSize
                                                       options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading)
                                                    attributes:stringAttributes context:nil];

    self.myLabelHeightConstraint.constant = ceilf(newRect.size.height);

J'ai tout essayé sur cette page et il me restait un cas pour un UILabel qui ne formatait pas correctement. En fait, le fait d’attribuer l'attribut attribué au texte a finalement résolu le problème.

1
Lil
Add Following methods in ur code for getting correct size of attribute string 
1.
    - (CGFloat)findHeightForText:(NSAttributedString *)text havingWidth:(CGFloat)widthValue andFont:(UIFont *)font
 {
    UITextView *textView = [[UITextView alloc] init];
    [textView setAttributedText:text];
    [textView setFont:font];
    CGSize size = [textView sizeThatFits:CGSizeMake(widthValue, FLT_MAX)];
    return size.height;

}

2. Call on heightForRowAtIndexPath method
     int h = [self findHeightForText:attrString havingWidth:yourScreenWidth andFont:urFont];
0
Anny
    NSAttributedString *attributedText =[[[NSAttributedString alloc]
                                          initWithString:joyMeComment.content
                                          attributes:@{ NSFontAttributeName: [UIFont systemFontOfSize:TextFont]}] autorelease];

    CGRect paragraphRect =
    [attributedText boundingRectWithSize:CGSizeMake(kWith, CGFLOAT_MAX)
                                 options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading)
                                 context:nil];
    contentSize = paragraphRect.size;

    contentSize.size.height+=10;
    label.frame=contentSize;

si le cadre de l'étiquette ne pas ajouter 10, cette méthode ne fonctionnera jamais! espérons que cela peut vous aider! bonne chance.

0
MICHEAL LV

Ok, donc j'ai passé beaucoup de temps à déboguer ceci. J'ai découvert que la hauteur de texte maximale définie par boundingRectWithSize autorisée à afficher du texte avec ma UITextView était inférieure à la taille du cadre.

Dans mon cas, le cadre ne dépasse pas 140 pt, mais UITextView tolère les textes tout au plus.

Je devais comprendre cela manuellement et coder en dur la "vraie" hauteur maximale.

Voici ma solution:

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
    NSString *proposedText = [textView.text stringByReplacingCharactersInRange:range withString:text];
    NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:proposedText];
    CGRect boundingRect;
    CGFloat maxFontSize = 100;
    CGFloat minFontSize = 30;
    CGFloat fontSize = maxFontSize + 1;
    BOOL fit;
    NSLog(@"Trying text: \"%@\"", proposedText);
    do {
        fontSize -= 1;
        //XXX Seems like trailing whitespaces count for 0. find a workaround
        [attributedText addAttribute:NSFontAttributeName value:[textView.font fontWithSize:fontSize] range:NSMakeRange(0, attributedText.length)];
        CGFloat padding = textView.textContainer.lineFragmentPadding;
        CGSize boundingSize = CGSizeMake(textView.frame.size.width - padding * 2, CGFLOAT_MAX);
        boundingRect = [attributedText boundingRectWithSize:boundingSize options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading context:nil];
        NSLog(@"bounding rect for font %f is %@; (max is %f %f). Padding: %f", fontSize, NSStringFromCGRect(boundingRect), textView.frame.size.width, 148.0, padding);
        fit =  boundingRect.size.height <= 131;
    } while (!fit && fontSize > minFontSize);
    if (fit) {
        self.textView.font = [self.textView.font fontWithSize:fontSize];
        NSLog(@"Fit!");
    } else {
        NSLog(@"No fit");
    }
    return fit;
}
0
Antzi

J'aimerais ajouter mes pensées puisque j'avais exactement le même problème.

J'utilisais UITextView car il disposait d'un meilleur alignement du texte (justifier, ce qui n'était pas disponible à l'époque dans UILabel), mais afin de "simuler" non-interactive-non-scrollable UILabel, Je désactiverais complètement le défilement, le rebondissement et l’interaction de l’utilisateur.

Bien sûr, le problème était que le texte était dynamique et que, même si la largeur était fixe, la hauteur devait être recalculée chaque fois que je définissais une nouvelle valeur de texte.

boundingRectWithSize ne fonctionnait pas du tout pour moi. D'après ce que j'ai pu voir, UITextView ajoutait une marge au-dessus de laquelle boundingRectWithSize ne serait pas pris en compte, d'où la hauteur extraite de boundingRectWithSize était plus petit que ce ne devrait être.

Comme le texte ne devait pas être mis à jour rapidement, il a simplement été utilisé pour certaines informations pouvant être mises à jour toutes les 2-3 secondes au maximum. J'ai donc choisi l'approche suivante:

/* This f is nested in a custom UIView-inherited class that is built using xib file */
-(void) setTextAndAutoSize:(NSString*)text inTextView:(UITextView*)tv
{
    CGFloat msgWidth = tv.frame.size.width; // get target's width

    // Make "test" UITextView to calculate correct size
    UITextView *temp = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, msgWidth, 300)]; // we set some height, really doesn't matter, just put some value like this one.
    // Set all font and text related parameters to be exact as the ones in targeted text view
    [temp setFont:tv.font];
    [temp setTextAlignment:tv.textAlignment];
    [temp setTextColor:tv.textColor];
    [temp setText:text];

    // Ask for size that fits :P
    CGSize tv_size = [temp sizeThatFits:CGSizeMake(msgWidth, 300)];

    // kill this "test" UITextView, it's purpose is over
    [temp release];
    temp = nil;

    // apply calculated size. if calcualted width differs, I choose to ignore it anyway and use only height because I want to have width absolutely fixed to designed value
    tv.frame = CGRectMake(tv.frame.Origin.x, tv.frame.Origin.y, msgWidth, tv_size.height );
}

* Le code ci-dessus n'est pas directement copié de ma source, je devais l'ajuster/le supprimer de tout ce qui n'était pas nécessaire pour cet article. Ne le prenez pas pour le copier-coller-et-ça-va-travailler-code.

Le désavantage évident est qu’il est possible d’allouer et de relâcher chaque appel.

Mais, l’avantage est que vous évitez de dépendre de la compatibilité de la façon dont boundingRectWithSize dessine et calcule la taille du texte et de son implémentation dans UITextView (ou UILabel que vous pouvez également remplacer par UITextView par UILabel). Tous les "bugs" que Apple pourrait avoir sont ainsi évités.

P.S. Il semblerait que vous n’ayez pas besoin de ce "temp" UITextView et que vous puissiez simplement demander sizeThatFits directement à la cible, mais cela n’a pas fonctionné pour moi. Bien que la logique dise que cela devrait fonctionner et que l'allocation/libération de UITextView temporaire ne sont pas nécessaires, ce n'est pas le cas. Mais cette solution a parfaitement fonctionné pour tous les textes que je voulais installer.

0
Sinisa