Discrepancy between sizeWithFont:constrainedToSize:lineBreakMode: and textView.contentSize.height - iphone

I am using the method [string sizeWithFont:constrainedToSize:lineBreakMode:] to estimate the height of a textView that I am resizing. However, it seems to consistently return the incorrect size. To debug, I wrote the following code:
self.textView.font = [UIFont systemFontOfSize:14.0]; // It was this before, anyways
NSLog(#"Real width: %lf %lf", self.textView.contentSize.width, self.textView.frame.size.width);
NSLog(#"Real height: %lf", self.textView.contentSize.height);
NSLog(#"Estimated width: %lf", kOTMessageCellTextWidth);
NSLog(#"Estimated height: %lf", ([message.message sizeWithFont:[UIFont systemFontOfSize:14.0]
constrainedToSize:CGSizeMake(kOTMessageCellTextWidth, CGFLOAT_MAX)
lineBreakMode:UILineBreakModeWordWrap].height));
However, the above code reveals that I am getting inconsistent results:
Real width: 223.000000 223.000000
Real height: 52.000000
Estimated width: 223.000000
Estimated height: 36.000000
Real width: 223.000000 223.000000
Real height: 142.000000
Estimated width: 223.000000
Estimated height: 126.000000
Real width: 223.000000 223.000000
Real height: 142.000000
Estimated width: 223.000000
Estimated height: 126.000000
I noticed in this similar question that (apparently) textView has some padding that constrains its actual width. The recommendation there was the decrease the width passed to sizeWithFont: by some number. The recommended number was 11.
My question: is there any way to actually retrieve this value programmatically, or is there some documentation that I missed specifying this number? It seems like this number should be available and shouldn't have to be guess-and-checked, but I can't find a reliable way to identify it.
Thanks in advance.

OK, after a bit of (re)search, I've come to this.
UITextView has 8px of padding on each side. But also line-height is not the same with UILabel (and sizeWithFont methods' result)
in my case the font was Arial and the size was 16pt and line heights of textview is 4.5points more than uilabel's line heights. So;
I get the result (cSize) from sizeWithFont method, with giving width reduced by 16 pixels
I calculated the lineCount by cSize.height / font.lineHeight
and used cSize.height + (lineCount * 4.5) + 16.0 as the final height for textview
code (not the exact code):
CGSize constraintSize = CGSizeMake(250.0 - 16.0, MAXFLOAT);
CGSize labelSize = [message.text sizeWithFont:[UIFont fontWithName:#"Arial" size:16.0] constrainedToSize:constraintSize lineBreakMode:UILineBreakModeWordWrap];
CGFloat lineCount = labelSize.height / [UIFont fontWithName:#"Arial" size:16.0].lineHeight;
CGFloat additional = lineCount * 4.5; // for 16.0 pt Arial for now
return labelSize.height + additional + 16.0; // TextView fix
I'm still having a little problems and do not like the way i solved it. But seems ok for this problem. And It would be great if someone comes with a reasonable solution for this.

The fix is simple, since UITextView has 8px of padding on each side, so when you are calculating text's true size, you should minus this 16px padding (8px on double sides) first, then the result is right. Please see following code snippet:
// self.descView is an UITextView
// UITEXTVIEW_TEXT_PADDING is 8.0
CGSize constrainedSize = CGSizeMake(self.descView.contentSize.width-UITEXTVIEW_TEXT_PADDING*2, MAXFLOAT);
CGSize trueSize = [self.descView.text sizeWithFont:self.descView.font
constrainedToSize:constrainedSize
lineBreakMode:UILineBreakModeWordWrap];
CGSize contentSize = self.descView.contentSize;
contentSize.height = trueSize.height;
self.descView.contentSize = contentSize;
frame = self.descView.frame;
frame.size.height = contentSize.height ;
self.descView.frame = frame;
The result should be right.

NSString sizeWithFont is computed using Core Text. UITextView uses a Webview (internally) to render its contents.
This discrepancy leads to all sorts of headaches. Such as choosing to break lines at different locations — you'll notice comma delimited lists, and certain mathematical expression strings will break on different symbols between Core Text and UITextView. This often leads to out-by-one-line errors when measuring spaces for your strings to fit.
And if you want international character support — well, you can forget about Core Text line heights matching UITextView (no matter how many combinations of paragraph style and line height you try!).
I have found that the only way to reliably estimate the size for a UITextView given an NSString is to... actually construct the UITextView and read its fitted size (minus the 8px padding).
Here's a UIKitLineMetrics class that does exactly that.
It keeps two "dummy" text views. You update the instance with your desired font and layout width, and you can read the single and multi-line sizes out of it.
UIKitLineMetrics.h
#import <UIKit/UIKit.h>
#interface UIKitLineMetrics : NSObject
#property (nonatomic, strong) NSMutableAttributedString *attributedString;
- (CGSize) UIKitLineSizeForText:(NSString*)text;
- (CGSize) UIKitSingleLineSizeForText:(NSString*)text;
- (void) updateWithFont:(UIFont*)font andWidth:(CGFloat)width;
#end
UIKitLineMetrics.m
#import "UIKitLineMetrics.h"
#interface UIKitLineMetrics ()
#property (nonatomic, strong) UITextView *measuringTextView;
#property (nonatomic, strong) UITextView *measuringSingleTextView;
#end
#implementation UIKitLineMetrics
#synthesize measuringTextView;
#synthesize measuringSingleTextView;
#synthesize attributedString;
- (id) init
{
self = [super init];
if( self )
{
attributedString = [[NSMutableAttributedString alloc] init];
measuringTextView = [[UITextView alloc] initWithFrame:CGRectZero];
measuringSingleTextView = [[UITextView alloc] initWithFrame:CGRectZero];
}
return self;
}
- (CGSize) UIKitLineSizeForText:(NSString*)text
{
measuringTextView.text = [text stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
CGSize sz = [measuringTextView sizeThatFits:CGSizeMake(measuringTextView.frame.size.width, CGFLOAT_MAX)];
return CGSizeMake(sz.width - 16, sz.height - 16);
}
- (CGSize) UIKitSingleLineSizeForText:(NSString*)text
{
measuringSingleTextView.text = [text stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
CGSize sz = [measuringSingleTextView sizeThatFits:CGSizeMake(measuringSingleTextView.frame.size.width, CGFLOAT_MAX)];
return CGSizeMake(sz.width - 16, sz.height - 16);
}
- (void) updateWithFont:(UIFont*)font andWidth:(CGFloat)width
{
measuringTextView.font = font;
measuringSingleTextView.font = font;
measuringTextView.frame = CGRectMake(0, 0, width, 500);
measuringSingleTextView.frame = CGRectMake(0, 0, 50000, 500);
}
#end

You could try checking the contentInset value (a property of UIScrollView).

I've also struggled with the problem. I've usually gotten by by passing the sizeWithFont method a size that's smaller than the actual size of the textView. Even when adding the textview's content insets to the size, it doesn't work. The size returned is always smaller.
charlie

I use following function without much problem:
+(float) calculateHeightOfTextFromWidth:(NSString*) text: (UIFont*)withFont: (float)width :(UILineBreakMode)lineBreakMode
{
[text retain];
[withFont retain];
CGSize suggestedSize = [text sizeWithFont:withFont constrainedToSize:CGSizeMake(width, FLT_MAX) lineBreakMode:lineBreakMode];
[text release];
[withFont release];
return suggestedSize.height;
}

Related

UILabel auto size error on iOS 7

I'm making my app transition to iOS 7 and have this method (already modified for iOS 7, using boundingRectWithSize...):
+ (CGSize)messageSize:(NSString*)message {
NSDictionary *attributes = #{NSFontAttributeName : [UIFont fontWithName:#"Futura-Medium" size:13]};
CGRect frame = [message boundingRectWithSize:CGSizeMake([PTSMessagingCell maxTextWidth], CGFLOAT_MAX) options:NSStringDrawingUsesFontLeading attributes:attributes context:nil];
return frame.size;
}
I am getting this appearance:
The message UILabel is being cut. It feels like line spacing is too big. It tried many other answers I found but none of them work.
If someone knows how to help me, I appreciate! ;)
Thanks!
Try changing NSStringDrawingUsesFontLeading as your option to NSStringDrawingUsesLineFragmentOrigin.
If you were only supporting iOS 6 and iOS 7, then I would definitely change all of your NSString's sizeWithFont:... to the NSAttributeString's boundingRectWithSize. Starting in iOS 6, the NSAttributedString's NSStringDrawing functions were introduced and they're threadsafe unlike the old NSString+UIKit methods we're used to (eg. sizeWithFont:..., etc), which were UIStringDrawing functions (and act unpredictably when you use them from a non-main thread. It'll save you a lot of headache if you happen to have a weird multi-threading corner case! Here's how I converted NSString's sizeWithFont:constrainedToSize::
What used to be:
NSString *text = ...;
CGFloat width = ...;
UIFont *font = ...;
CGSize size = [text sizeWithFont:font
constrainedToSize:(CGSize){width, CGFLOAT_MAX}];
Can be replaced with:
NSString *text = ...;
CGFloat width = ...;
UIFont *font = ...;
NSAttributedString *attributedText =
[[NSAttributedString alloc]
initWithString:text
attributes:#
{
NSFontAttributeName: font
}];
CGRect rect = [attributedText boundingRectWithSize:(CGSize){width, CGFLOAT_MAX}
options:NSStringDrawingUsesLineFragmentOrigin
context:nil];
CGSize size = rect.size;
Please note the documentation mentions:
In iOS 7 and later, this method returns fractional sizes (in the size
component of the returned CGRect); to use a returned size to size
views, you must use raise its value to the nearest higher integer
using the ceil function.
So to pull out the calculated height or width to be used for sizing views, I would use:
CGFloat height = ceilf(size.height);
CGFloat width = ceilf(size.width);
I think you are updating your label frame from either viewDidLoad or viewWillAppear, so it is not working.
if you will update frame of label from viewDidAppear method then you will get updated frame of label.
I am not sure why this is happened, I think it is iOS 7 bug.
Try this
+ (CGSize)messageSize:(NSString*)message {
CGSize nameSize = [message sizeWithFont:[UIFont fontWithName:#"Futura-Medium" size:13]
constrainedToSize:CGSizeMake(maxWidth, maxHeight) lineBreakMode:NSLineBreakByWordWrapping];
NSLog(#"width = %f, height = %f", nameSize.width, nameSize.height);
return nameSize;
}

How to get the size of a NSString

A "quicky": how can I get the size (width) of a NSString?
I'm trying to see if the string width of a string to see if it is bigger than a given width of screen, case in which I have to "crop" it and append it with "...", getting the usual behavior of a UILabel. string.length won't do the trick since AAAAAAAA and iiiiii have the same length but different sizes (for example).
I'm kind of stuck.
Thanks a lot.
This is a different approach. Find out the minimum size of the text so that it won't wrap to more than one line. If it wraps to over one line, you can find out using the height.
You can use this code:
CGSize maximumSize = CGSizeMake(300, 9999);
NSString *myString = #"This is a long string which wraps";
UIFont *myFont = [UIFont fontWithName:#"Helvetica" size:14];
CGSize myStringSize = [myString sizeWithFont:myFont
constrainedToSize:maximumSize
lineBreakMode:self.myLabel.lineBreakMode];
300 is the width of the screen with a little space for margins. You should substitute your own values for font and size, and for the lineBreakMode if you're not using IB.
Now myStringSize will contain a height which you can check against the height of something you know is only 1 line high (using the same font and size). If it's bigger, you'll need to cut the text. Note that you should add a ... to the string before you check it again (adding the ... might push it over the limit again).
Put this code in a loop to cut the text, then check again for the correct height.
Use below method.
Objective-C
- (CGSize)findHeightForText:(NSString *)text havingWidth:(CGFloat)widthValue andFont:(UIFont *)font {
CGSize size = CGSizeZero;
if (text) {
CGRect frame = [text boundingRectWithSize:CGSizeMake(widthValue, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:#{ NSFontAttributeName:font } context:nil];
size = CGSizeMake(frame.size.width, frame.size.height + 1);
}
return size;
}
Swift 3.0
func findHeight(forText text: String, havingWidth widthValue: CGFloat, andFont font: UIFont) -> CGSize {
var size = CGSizeZero
if text {
var frame = text.boundingRect(withSize: CGSize(width: widthValue, height: CGFLOAT_MAX), options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
size = CGSize(width: frame.size.width, height: frame.size.height + 1)
}
return size
}
You need to use Core Graphics to measure the string, as rendered in your specified font and size. See the answers to Measuring the pixel width of a string for a walkthrough.
sizeWithFont:constrainedToSize:lineBreakMode
is deprecated now. Use below code snippet,
UIFont *font=[UIFont fontWithName:#"Arial" size:16.f];
NSString *name = #"APPLE";
CGSize size = [name sizeWithAttributes:#{NSFontAttributeName:font}];
For whatever its worth --- I think the OP takes the wrong way to get there... if the measurement of width only serves to find the place where text should be clipped, and followed by ellipsis --- then OP should be aware of that this facility is implemented in all Text Views in Cocoa...
Pay attention to this enumeration:
typedef NS_ENUM(NSUInteger, NSLineBreakMode) {
NSLineBreakByWordWrapping = 0, // Wrap at word boundaries, default
NSLineBreakByCharWrapping, // Wrap at character boundaries
NSLineBreakByClipping, // Simply clip
NSLineBreakByTruncatingHead, // Truncate at head of line: "...wxyz"
NSLineBreakByTruncatingTail, // Truncate at tail of line: "abcd..."
NSLineBreakByTruncatingMiddle // Truncate middle of line: "ab...yz"
} API_AVAILABLE(macos(10.0), ios(6.0), watchos(2.0), tvos(9.0));
By setting the line breaking mode of your text-field or text view to NSLineBreakByTruncatingTail, you'll achieve what you want, and probably at higher quality, without implementing yourself.

How to calculate the height of the text rectangle from an NSString?

I know there is this one:
sizeWithFont:minFontSize:actualFontSize:forWidth:lineBreakMode:
But since the CGSize always has the same height and doesn't adjust to any shrinked text or whatsoever, the CGSize is not telling how heigh the text is.
Example: Make a UILabel with 320 x 55 points and put a loooooooooooooong text in there. Let the label shrink the text down. Surprise: CGSize.height remains the same height even if the text is so tiny that you need a microscope.
Ok so after banging my head against my macbook pro which is half way broken now, the only think that can help is that nasty actualFontSize. But the font size is in pica I think, it's not really what you get on the screen, isn't it?
When that font size is 10, is my text really 10 points heigh at maximum? Once in a while I tried exactly that, and as soon as the text had a y or some character that extends to below (like that tail of an y does), it is out of bounds and the whole text is bigger than 10 points.
So how would you calculate the real text height for a single line uilabel without getting a long beard and some hospital experience?
Try this code:
CGSize maximumSize = CGSizeMake(300, 9999);
NSString *myString = #"This is a long string which wraps";
UIFont *myFont = [UIFont fontWithName:#"Helvetica" size:14];
CGSize myStringSize = [myString sizeWithFont:myFont
constrainedToSize:maximumSize
lineBreakMode:self.myLabel.lineBreakMode];
from my answer here
It uses a different method, and sets up a very high CGSize at the start (which is then shrunk to fit the string)
Important: As of iOS 7.0 the following method is deprecated.
sizeWithFont:minFontSize:actualFontSize:forWidth:lineBreakMode:
Use the below code instead
CGRect frame = [cellText boundingRectWithSize:CGSizeMake(568,320) options:NSStringDrawingUsesLineFragmentOrigin attributes:#{NSFontAttributeName:[UIFont systemFontOfSize:32.0f]} context:nil];
float height = frame.size.height;
Sounds like after you get the actual font size from that function call, you need to call again with that new size:
NSString* yourString = #"SomeString";
float actualSize;
[yourString sizeWithFont:yourFont
minFontSize:minSize
actualFontSize:&actualSize
forWidth:rectWidth
lineBreakMode:breakMode];
CGSize size = [yourString sizeWithFont:[UIFont fontWithName:fontName size:actualSize]];
Also have you set label.numberOfLines = 0; ?
After running this code frame.size will have the height and width of your nsstring exactly..
NSString *text = #"This is my Mac";
textFont = [NSFont fontWithName:#"HelveticaNeue-Medium" size: 80.f];
textColor = [NSColor yellowColor];
NSDictionary *attribs = #{ NSForegroundColorAttributeName:textColor,
NSFontAttributeName: textFont };
CGRect frame = [text boundingRectWithSize:CGSizeMake(0,0) options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesDeviceMetrics attributes:attribs context:nil];

How to figure out the font size of a UILabel when -adjustsFontSizeToFitWidth is set to YES?

When myLabel.adjustsFontSizeToFitWidth = YES, UILabel will adjust the font size automatically in case the text is too long for the label. For example, if my label is just 100px wide, and my text is too long to fit with the current font size, it will shrink down the font size until the text fits into the label.
I need to get the actual displayed font size from UILabel when the font size got shrunk down. For example, let's say my font size was actually 20, but UILabel had to shrink it down to 10. When I ask UILabel for the font and the font size, I get my old font size (20), but not the one that's displayed (10).
I'm not sure if this is entirely accurate, but it should be pretty close, hopefully. It may not take truncated strings into account, or the height of the label, but that's something you might be able to do manually.
The method
- (CGSize)sizeWithFont:(UIFont *)font minFontSize:(CGFloat)minFontSize actualFontSize:(CGFloat *)actualFontSize forWidth:(CGFloat)width lineBreakMode:(UILineBreakMode)lineBreakMode
will return the text size, and notice that it also has a reference parameter for the actual font size used.
In case anybody still needs the answer.
In iOS9 you can use boundingRectWithSize:options:context: to calculate actual font size. Note that context.minimumScaleFactor should not be 0.0 for scaling to work.
- (CGFloat)adjustedFontSizeForLabel:(UILabel *)label {
NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithAttributedString:label.attributedText];
[text setAttributes:#{NSFontAttributeName:label.font} range:NSMakeRange(0, text.length)];
NSStringDrawingContext *context = [NSStringDrawingContext new];
context.minimumScaleFactor = label.minimumScaleFactor;
[text boundingRectWithSize:label.frame.size options:NSStringDrawingUsesLineFragmentOrigin context:context];
CGFloat adjustedFontSize = label.font.pointSize * context.actualScaleFactor;
return adjustedFontSize;
}
For one-line UILabel works fine this simple solution:
//myLabel - initial label
UILabel *fullSizeLabel = [UILabel new];
fullSizeLabel.font = myLabel.font;
fullSizeLabel.text = myLabel.text;
[fullSizeLabel sizeToFit];
CGFloat actualFontSize = myLabel.font.pointSize * (myLabel.bounds.size.width / fullSizeLabel.bounds.size.width);
//correct, if new font size bigger than initial
actualFontSize = actualFontSize < myLabel.font.pointSize ? actualFontSize : myLabel.font.pointSize;
Swift 5
For one-line UILabel
extension UILabel {
var actualFontSize: CGFloat {
//initial label
let fullSizeLabel = UILabel()
fullSizeLabel.font = self.font
fullSizeLabel.text = self.text
fullSizeLabel.sizeToFit()
var actualFontSize: CGFloat = self.font.pointSize * (self.bounds.size.width / fullSizeLabel.bounds.size.width);
//correct, if new font size bigger than initial
actualFontSize = actualFontSize < self.font.pointSize ? actualFontSize : self.font.pointSize;
return actualFontSize
}
}
Getting the actual font size is then as simple as:
let currentLabelFontSize = myLabel.actualFontSize
UILabel *txtLabel = [[UILabel alloc] initWithFrame:rectMax];
txtLabel.numberOfLines = 1;
txtLabel.font = self.fontMax;
txtLabel.adjustsFontSizeToFitWidth = YES;
txtLabel.minimumScaleFactor = 0.1;
[txtLabel setText:strMax];
UILabel *fullSizeLabel = [UILabel new];
fullSizeLabel.font = txtLabel.font;
fullSizeLabel.text = txtLabel.text;
fullSizeLabel.numberOfLines = 1;
[fullSizeLabel sizeToFit];
CGFloat actualFontSize = txtLabel.font.pointSize * (txtLabel.bounds.size.width / fullSizeLabel.bounds.size.width);
actualFontSize = actualFontSize < txtLabel.font.pointSize ? actualFontSize : txtLabel.font.pointSize;
// the actual font
self.fontMax = [UIFont fontWithName:self.fontMax.fontName size:actualFontSize];
my code works great, part from #Igor

String length with given font to fit UITextView 2 - The Return

In this question I asked for a good way to truncate a string to fit a given UITextView. Since there was no way provided by the SDK directly, I've ended up writing the recursive method below (only called by the following public method). However, this doesn't work unless I subtract a fudge factor of 15 (kFudgeFactor) from the field width when calculating the string's height. If I don't do that, the string returned is actually too long for the field, and displays in an extra line below it. Anyone any idea why, and what I should really use instead of this fudge factor?
#pragma mark Size string to fit the new view
#define kFudgeFactor 15.0
#define kMaxFieldHeight 9999.0
// recursive method called by the main API
-(NSString*) sizeStringToFit:(NSString*)aString min:(int)aMin max:(int)aMax
{
if ((aMax-aMin) <= 1)
{
NSString* subString = [aString substringToIndex:aMin];
return subString;
}
int mean = (aMin + aMax)/2;
NSString* subString = [aString substringToIndex:mean];
CGSize tallerSize = CGSizeMake(self.frame.size.width-kFudgeFactor,kMaxFieldHeight);
CGSize stringSize = [subString sizeWithFont:self.font constrainedToSize:tallerSize lineBreakMode:UILineBreakModeWordWrap];
if (stringSize.height <= self.frame.size.height)
return [self sizeStringToFit:aString min:mean max:aMax]; // too small
else
return [self sizeStringToFit:aString min:aMin max:mean];// too big
}
-(NSString*)sizeStringToFit:(NSString*)aString
{
CGSize tallerSize = CGSizeMake(self.frame.size.width-kFudgeFactor,kMaxFieldHeight);
CGSize stringSize = [aString sizeWithFont:self.font constrainedToSize:tallerSize lineBreakMode:UILineBreakModeWordWrap];
// if it fits, just return
if (stringSize.height < self.frame.size.height)
return aString;
// too big - call the recursive method to size it
NSString* smallerString = [self sizeStringToFit:aString min:0 max:[aString length]];
return smallerString;
}
UIScrollView seems to use a fixed 8-pixel inset on both sides. This is independent of alignment or font size (based on testing & observation, not any explicit knowledge of the internals).
So it seems you are right to use your fudge factor, but it should probably be 16.0, not 15.0.
This is probably because the frame of the UIView is not the same size as the content view.
UITextView subclasses from UIScrollView.