web-dev-qa-db-ja.com

NSAttributedStringのboundingRectWithSizeが間違ったサイズを返す

属性付き文字列の四角形を取得しようとしていますが、boundingRectWithSize呼び出しは、渡されたサイズを考慮せず、大きな高さ(長い文字列)ではなく単一行の高さの四角形を返します。以下のコードのように、高さの非常に大きな値と0も渡すことで実験しましたが、返される四角形は常に同じです。

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

これは壊れていますか、それともラップされたテキストの四角形を返すために何か他のことをする必要がありますか?

146
RunLoop

正しいオプションを提供していないようです。ラベルをラッピングするには、少なくとも以下を提供します。

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

注:元のテキストの幅が300.f未満の場合、行の折り返しは行われないため、バインドされたサイズが正しいことを確認してください。そうしないと、間違った結果が得られます。

301
Ed McManus

その方法は多くの点でバグがあるようです。 1つは、お気付きのとおり、幅の制約を尊重していません。別の方法として、すべての属性がNSObjectタイプであると想定しているためにクラッシュするのを確認しました(たとえば、_isDefaultFaceCTFontRefに渡そうとしました)。また、文字列描画コンテキストが提供されると、バックグラウンドで可変属性文字列にnil値属性を追加しようとするため、クラッシュすることもあります。

この方法を完全に避けることをお勧めします。描画する必要のある文字列ごとにフレームセッターを作成するオーバーヘッドを処理できる場合は、Core Textを直接使用して文字列サイズを推定できます。幅の制約も厳密には尊重しませんが、私の経験では、数ピクセル以内に収まるようです。

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);
117
warrenm

何らかの理由で、boundingRectWithSizeは常に間違ったサイズを返します。私は解決策を見つけました。 UItextView -sizeThatFitsには、テキストセットの適切なサイズを返すメソッドがあります。したがって、boundingRectWithSizeを使用する代わりに、ランダムなフレームでUITextViewを作成し、それぞれの幅とCGFLOAT_MAX高さでそのsizeThatFitsを呼び出します。適切な高さを持つサイズを返します。

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

Whileループでサイズを計算している場合、n個のUITextViewが作成されるため、autoreleaseプールに追加することを忘れないでください。autoreleasepoolを使用しない場合、アプリのランタイムメモリが増加します。

44
shoan

エド・マクマナスは確かにこれを機能させるための鍵を提供してくれました。動作しないケースを見つけました

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は正しい高さを持ちません。 anotherStringstringに追加される)は、属性辞書。これはanotherStringの正当な初期化子ですが、boundingRectWithSize:はこの場合、正確なサイズを与えません。

31
Paul Heller

長い調査の後の私の最終決定:
- boundingRectWithSize関数は、途切れない文字シーケンスのみに対して正しいサイズを返します!文字列にスペースなどが含まれる場合(Apple "グリフの一部"と呼ばれる)-テキストの表示に必要な実際のサイズの四角形を取得することは不可能です!
文字列のスペースを文字に置き換え、すぐに正しい結果を得ました。

Appleは次のように述べています: https://developer.Apple.com/documentation/foundation/nsstring/1524729-boundingrectwithsize

「このメソッドは、文字列内のグリフの実際の境界を返します。一部のグリフ(たとえば、スペース)は、渡されたサイズで指定されたレイアウト制約とオーバーラップできます。返されたCGRectは、サイズパラメータの幅の値を超えることができます。」

したがって、実際の長方形を計算する別の方法を見つける必要があります...


長い調査プロセスのソリューションがついに見つかりました!!! UITextViewに関連するすべてのケースで適切に機能するかどうかはわかりませんが、主な重要なものが検出されました!

boundingRectWithSize関数とCTFramesetterSuggestFrameSizeWithConstraints(および他の多くのメソッド)は、正しい長方形が使用されたときにサイズとテキスト部分を正しく計算します。たとえば-UITextViewにはtextView.bounds.size.widthがあり、この値は、UITextViewでテキストを描画するときにシステムが使用する実際の長方形ではありません。

非常に興味深いパラメーターを見つけて、コードで簡単な計算を実行しました。

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

そして魔法の仕組み-私のテキストはすべて正しく計算されました!楽しい!

私はこれらの提案のどれにも運がなかった。私の文字列にはユニコードの箇条書きが含まれていて、それらが計算で悲嘆を引き起こしていると思われます。 UITextViewが描画をうまく処理していることに気づいたので、その計算を活用するためにそれを探しました。 NSStringの描画メソッドほど最適ではないかもしれませんが、少なくとも正確です。また、-sizeThatFits:を呼び出すためだけにUITextViewを初期化するよりもわずかに最適です。

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

テールを切り捨てて境界ボックスを取得したい場合は、 この質問 が役立ちます。

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.

Swift 4バージョン

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

CTFramesetterでテキストを測定すると、整数サイズが提供され、絵文字やその他のUnicode文字を適切に処理できるため、最適に機能します。

9
David Rees

@warrenmフレームセッターメソッドが機能しなかったと言って申し訳ありません。

私はこれを手に入れました。この関数は、指定された幅のiphone/iPad SDKのNSAttributedStringの文字列範囲に必要なフレームサイズを決定するのに役立ちます:

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));
}

HADDAD ISSAのおかげ>>> http://haddadissa.blogspot.in/2010/09/compute-needed-heigh-for-fixed-width-of.html

7
Karan Alangat

望ましい解決策は改行を処理しないことがわかりました。

私はこのアプローチがすべての場合に機能することを発見しました:

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

BoundingRectWithSizeを実際に機能させるには、NSAttributedStringのすべての部分に、少なくともNSFontAttributeNameとNSForegroundColorAttributeNameが設定された辞書が設定されている必要があります。

私はそれがどこにも文書化されていないのを見ています。

6
Ben Wheeler

これらの手法を使用して正確なサイズを取得できないという同じ問題があり、それを機能させるためにアプローチを変更しました。

切り捨てられることなく適切に表示されるように、スクロールビューに収めようとしている長い属性の文字列があります。テキストを確実に機能させるためにしたことは、制約として高さをまったく設定せず、代わりに固有のサイズが引き継ぐことを許可することでした。これで、テキストは切り捨てられることなく正しく表示され、高さを計算する必要がなくなりました。

高さを確実に取得する必要がある場合は、非表示のビューとこれらの制約を作成し、制約が適用されたらフレームの高さを取得すると思います。

4
Brennan

ゲームに少し遅れていますが、Finderでファイルを編集するようなフォーカスリングを作成するために、属性文字列の周りに収まる境界ボックスを見つける方法を見つけようとしています。文字列の末尾にスペースがあるか、文字列内に複数のスペースがある場合、私が試したすべてが失敗しました。 boundingRectWithSizeは、CTFramesetterCreateWithAttributedStringと同様に、これに対してひどく失敗します。

NSLayoutManagerを使用すると、これまでに見つかったすべてのケースで次のコードがうまくいくようで、文字列を完全に区切る四角形を返します。ボーナス:テキストを選択すると、選択範囲の端が、返される四角形の境界まで上がります。以下のコードでは、NSTextViewのlayoutManagerを使用しています。

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));

すべてのフォントで完全に動作します!

2
Dmitry Coolerov

私が気づいたことの1つは、(CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options attributes:(NSDictionary *)attributes context:(NSStringDrawingContext *)contextから返される四角形の幅が、渡されたものよりも大きいことです。これが発生すると、文字列が切り捨てられます。私はこのように解決しました:

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;

もう少しコンテキストについては。複数行のテキストがあり、表示するのに適切な高さを見つけようとしていました。boundRectWithSizeは、指定した幅よりも大きい幅を返すことがありました。切り捨てられます。 boundingRectWithSizeが間違った幅を使用したときのテストから、高さが短くなる量は1行でした。そのため、幅が大きいかどうかを確認し、フォントのlineHeightを追加して、切り捨てを回避するのに十分なスペースを提供します。

1
odyth

私は同じ問題を抱えていましたが、拘束された高さが正しく設定されていることを認識しました。だから私は次のことをしました:

-(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);

このページですべて試してみましたが、UILabelが正しくフォーマットされていない場合が1つありました。実際にラベルにattributedTextを設定すると、最終的に問題が修正されました。

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;

ラベルのフレームに10を追加しない場合、このメソッドは機能しません!これがあなたを助けることを願っています!グーグル運。

0
MICHEAL LV

わかりましたので、これをデバッグするのに多くの時間を費やしました。 boundingRectWithSizeでテキストを表示できるUITextViewで定義されている最大テキスト高さが、フレームサイズよりも低いことがわかりました。

私の場合、フレームは最大140ptですが、UITextViewは最大131ptのテキストを許容します。

私は手動でそれを把握し、「実際の」最大高さをハードコーディングする必要がありました。

ここに私の解決策があります:

- (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

私はまったく同じ問題を抱えていたので、考えを追加したいと思います。

私はUITextViewを使用していましたが、テキストの配置が優れていたので(当時はUILabelでは使用できませんでした)、非インタラクティブでスクロールできないUILabelを「シミュレート」するには、完全にスクロール、バウンス、およびユーザーインタラクション。

もちろん、問題は、テキストが動的であり、幅が固定される一方で、新しいテキスト値を設定するたびに高さを再計算する必要があることでした。

boundingRectWithSizeは私にはまったくうまくいきませんでした。私が見ることができることから、UITextViewは、boundingRectWithSizeがカウントに入らないマージンを上に追加していたため、boundingRectWithSizeから取得した高さが本来よりも小さくなりました。

テキストは迅速に更新されるべきではなかったため、2〜3秒ごとに更新される可能性のある情報に使用されるだけなので、次のアプローチを決定しました。

/* 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 );
}

*上記のコードはソースから直接コピーされたものではなく、この記事に必要のない他の多くのコードから調整/消去する必要がありました。コピー・アンド・アンド・アンド・イット・ウィル・ワーク・コードとして受け取らないでください。

明らかな欠点は、呼び出しごとにallocとreleaseがあることです。

ただし、boundingRectWithSizeがテキストを描画し、そのサイズを計算する方法とUITextView(またはUILabelUITextViewに置き換えるだけで使用できるUILabel)の実装との互換性に依存することを回避できるという利点があります。 Appleが持つ可能性のある「バグ」は、この方法で回避されます。

追伸この "temp" UITextViewは必要ないはずで、ターゲットからsizeThatFitsに直接問い合わせることができるように思えますが、それはうまくいきませんでした。ロジックはそれが機能するはずで、一時的なUITextViewのalloc/releaseは必要ないと言うでしょうが、必要ありませんでした。しかし、このソリューションは、私が設定するテキストに対して完璧に機能しました。

0
Sinisa