Calculate UILabel height gives wrong values - objective-c

I am trying to "forecast" what the size of a label is going to be (width is known, only height).
I am trying to use this:
CGSize possibleSize = [text sizeWithFont:[UIFont fontWithName:#"American Typewriter" size:16]
constrainedToSize:CGSizeMake(self.collectionView.frame.size.width ,9999)
lineBreakMode:NSLineBreakByWordWrapping];
Which gives very inaccurate results (even changing the fonts and the size won't make it better, for example I get the height 30 instead of 80).
I have read that other people also don't get good results with it. Am I using it right?
I have also tried:
UILabel *test=[[UILabel alloc] initWithFrame:self.collectionView.frame];
test.text=[dic objectForKey:#"text"];
test.font=[UIFont fontWithName:#"American Typewriter" size:12];
[test sizeToFit];
NSLog(#"%f",test.frame.size.height);
I have to know what the height is going to be, and this method is not even close.
Is there some other way that gives reasonable results?

This sizeWithFont method is now deprecated, this new method works best
NSString *content = **Whatever your label's content is expected to be**
CGSize maximumLabelSize = CGSizeMake(self.label.frame.size.width, 9999);
NSDictionary *stringAttributes = [NSDictionary dictionaryWithObject:[UIFont fontWithName:#"American Typewriter" size:16] forKey: NSFontAttributeName];
CGSize expectedLabelSize = [content boundingRectWithSize:maximumLabelSize options:NSStringDrawingTruncatesLastVisibleLine|NSStringDrawingUsesLineFragmentOrigin attributes:stringAttributes context:nil].size;
CGFloat labelHeight = expectedLabelSize.height;
where labelHeight is the height that the label will be calculated from the amount of text due to load into the label.
I hope this helps, cheers, Jim.

#matt you are onto something, but I'll add that you should set the number of lines on that label to 0, before you calculate sizeWithFont.
You may also try to replace
CGSizeMake(self.collectionView.frame.size.width ,9999)
with
CGSizeMake(self.collectionView.bounds.size.width ,FLT_MAX)
the key element being "bounds" instead of frame.
Lastly, ensure you're not getting nothing for [dic objectForKey:#"text"].
Use
NSAssert(dic[#"text"]);
if ([dic[#"text"] isEqualToString:""]) {
; //empty string
}

First of all you need to allow a UILabel to be multiline, by default it is one line (set number of line to 0)
yourLabel.numberOfLines = 0; // means - label can be multiline
Secondly, it looks like you are calculating size for a bigger font that the label has in fact. Consider using the same size so set the calculations correctly.
Moreover if you support only iOS 7 and newer consider usage of sizeWithAttributes method introduced in iOS 7 or boundingRectWithSize:options:attributes:context - replacement for the method you use from iOS 7 and further for text size calculations.
Finally (advice) if you need that height value only for setting height of the label maybe you should consider using auto layout (it will be much easier to deal with).

Related

How to find the position of the last text line in a multiline UILabel or otherwise have UILabel have 0 padding

I have a UILabel that has both -numberOfLines set to 3 and text-size auto shrink and I need to align another UIView to this UILabel's last line of text. That is, I might need to align to the y position of line 0, 1 or 2, depending on the text inside the label (and the distance between these lines of text may vary depending on whether the text is long enough that it triggered font resizing).
But:
UILabel doesn't expose a contentSize
the label's bounds extend past the last line of text (there seems to be a content inset), so aligning to the bounds won't work.
subclassing UILabel and doing something like this:
- (void)drawTextInRect:(CGRect)rect {
UIEdgeInsets insets = {0., 0., -30., 0.};
return [super drawTextInRect:UIEdgeInsetsInsetRect(rect, insets)];
}
just happens to work for the case where I have 3 lines and the font size was auto shrunk, but I still can'r figure out a generic way of subtracting insets for the general case, regardless of text size. And I don't seem to be able to use -boundingRectWithSize:options:context: either: it either returns a single line equivalent rect or, If I play around with the options, a a rect the same size of the original label (that is, including the extra insets I'm trying to get rid of). Mind you, the idea behind removing any insets is that if I have no way of knowing where the last line of text is, at least I can remove any insets in the label so that the last line of text aligns with the label's bounds.origin.y + bounds.size.height.
Any thoughts?
I don't know if the problem was that originally I was using boundingRectWithSize on non-attributed text or what but now this seems to work:
NSString *text = <get text from CoreData>;
NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:#{NSFontAttributeName: self.titleLabel.font}];
CGRect rect = [attributedText boundingRectWithSize:self.titleLabel.frame.size
options:NSStringDrawingUsesLineFragmentOrigin
context:nil];
if (!rect.size.height || rect.size.height > self.titleLabel.frame.size.height) {
attributedText = [[NSAttributedString alloc] initWithString:text attributes:#{NSFontAttributeName: [UIFont boldSystemFontOfSize:self.titleLabel.font.pointSize * self.titleLabel.minimumScaleFactor]}];
rect = [attributedText boundingRectWithSize:self.titleLabel.frame.size
options:NSStringDrawingUsesLineFragmentOrigin
context:nil];
}
self.titleLabel.frame = rect;
self.titleLabel.attributedText = attributedText;
While this doesn't really find the position of the bottom of the last line of text in the UILabel (the label still adds some padding at the bottom... not sure if to account for descenders), it adjusts the label's bounds close enough to the bottom that I can at least align based on bounds.origin.y + bounds.size.height and it looks good enough.

Calculate Cell Height without Using Fixed Values

I need to calculate the height for a UITableViewCell with includes multiple rows of wrapping text. So far I couldn't find a 100% exact way to do this. Furthermore the technique I currently use relies heavily on fixed values:
NSString *cellText;
cellText = #"Very long multi line text in this String ...";
UIFont *cellFont = [UIFont fontWithName:#"Helvetica" size:14.0];
CGSize constraintSize = CGSizeMake(280.0f, MAXFLOAT);
CGSize labelSize = [cellText sizeWithFont:cellFont constrainedToSize:constraintSize lineBreakMode:NSLineBreakByWordWrapping];
return labelSize.height + 32;
Fixed values used in just in these few lines:
Fontsize (14)
Font Type (Helvetica)
CGSize, Width (280.0f)
Random padding value that I found is needed (32)
Is there a nice way to rewrite this in a way that all these fixed values are fetched and calculated dynamically?
That would help a lot to make the layout more responsive and the code reusable going forward. I appreciate any best practices.
Your code seems good but u missed is cell appropriate calculation.U need to add label's topmost postion and label's bottommost postion in cell(parentView).
Just calculate like this
return labelSize.height + 2*yourLabelInCell.frame.origin.y //here label's topmost postion and label's bottommost postion in cell added
Label will adjust appropriately in cell.

NSParagraphStyle line spacing ignored

A simple test that is failed: Make a new project with just one subview (UITextView) and put the following in:
- (void)viewDidLoad
{
[super viewDidLoad];
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.lineHeightMultiple = 50.f;
paragraphStyle.lineSpacing = 100.f;
paragraphStyle.minimumLineHeight = 200.f;
paragraphStyle.maximumLineHeight = 500.f;
UIFont *font = [UIFont fontWithName:#"AmericanTypewriter" size:24.f];
self.textView.attributedText = [[NSAttributedString alloc] initWithString:
#"This is a test.\n Will I pass?" attributes:
#{NSParagraphStyleAttributeName : paragraphStyle, NSFontAttributeName : font}];
}
Line spacing is the same as if the attribute were not there. Has anything got this to work successfully? I put in ridiculous numbers just to show that it won't change...
This is a bug in NSHTMLWriter which is the private class which UITextView uses to convert attributedText into HTML. Internally it displays this HTML via a UIWebDocumentView. Read more on the inner workings of UITextView in my writeup here: http://www.cocoanetics.com/2012/12/uitextview-caught-with-trousers-down/
The problem comes from an easy to miss speciality in the font CSS shorthand. If you specify a pixel size with the font shorthand then this sets BOTH the font-size as well as the line-height. Since NSHTMLWriter puts the font AFTER the line-height this causes the line-height to be cancelled out by the font size.
See here for my Radar which includes the full analysis of the bug: http://www.cocoanetics.com/2012/12/radar-uitextview-ignores-minimummaximum-line-height-in-attributed-string/
I suggest you file a bug report as well and mention my Radar #12863734.
I don't know if this is enough for your purposes but I could adjust the line spacing by setting the minimum and maximum line height. Furthermore to use a font I put it into the font property of the text view rather than passing it as the value of NSFontAttributeName in the attributes dictionary. (Maybe this part is not (well) documented?)
About your attributes
lineSpacing is calculated from the bottom of the line to the bottom of the upper line and that space is constrained to values between minimumLineHeight and miximumLineHeight. What I am trying to say is that maybe some values in your attributes are cancelling or overriding others.
Also if you need to just adjust the spacing between line you probably don't need to use paragraphStyle.lineHeightMultiple :)
The code
This worked for me:
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.minimumLineHeight = 35.f;
paragraphStyle.maximumLineHeight = 35.f;
UIFont *font = [UIFont fontWithName:#"AmericanTypewriter" size:18.f];
NSString *string = #"This is a test.\nWill I pass?\n日本語のもじもあるEnglish\nEnglish y Español";
NSDictionary *attributtes = #{
NSParagraphStyleAttributeName : paragraphStyle,
};
self.textView.font = font;
self.textView.attributedText = [[NSAttributedString alloc] initWithString:string
attributes:attributtes];
Additional Notes
There seems to be a situation with Japanese/Chinesse and maybe other characters mixed with alphabet characters in the same line. It will make that line to have a bigger leading to solve that you need to set up the minimum and maximum line height as I did.
You can see the problem when rendering my example without attributes.
Setting maximumLineHeight seems to resolve this issue for me;
CGFloat fontSize = 22.f;
titleLabel.font = [UIFont boldSystemFontOfSize:fontSize];
NSMutableParagraphStyle *paragraphStyle = [[[NSMutableParagraphStyle alloc] init] autorelease];
paragraphStyle.maximumLineHeight = fontSize/2;
titleLabel.attributedText = [[[NSAttributedString alloc]
initWithString:#"This is a test.\nWill I pass?"
attributes: #{ NSParagraphStyleAttributeName : paragraphStyle,
NSFontAttributeName : titleLabel.font}]
autorelease];
For this particular string you need to set paragraphSpacing instead. What's about lineSpacing, I believe it's just not supported yet on iOS.
As nacho4d answered, in iOS 6 you need to use minimumLineHeight and maximumLineHeight and set font directly in UITextView, not in NSAttributedString as line height in that case will be overridden.
Please note that when you set font in UITextView, the "editable" property of UITextView should be set to YES, in other case attributed text would not be affected.
These issues are present only in iOS 6. In iOS 7 and above everything is ok;
In my case, none of the paragraph styling was working. The fix was to set the attributed text on the label AFTER doing any frame adjustments on the label. :)

How to fit a text with various length in a UITableViewCell?

What I have is:
a NSString which can have any length between 1 and 400 characters
a UITableViewCell (custom layout)
I tried using an UILabel with multiple lines, set the text, and call sizeToFit. That doesn't work always, most of the time the UILabel just clips off the part of the string that doesn't fit. Also, due the varying length of the text I'd need differently sized UITableViewCells, and at the time "tableView: cellForRowAtIndexPath:" is called I don't know what the height will be.
So what I need is a non-scrolling UI element which is able to display text and resizes its height (the width should remain constant) to exactly fit the text. As mentioned the sizeToFit method produces mostly garbage.
You can use SizeWithFont: to calculate the desired height for your cell and store it in an Array so that you can return that height in HeightForRowAtIndexPath. If you need to update the text, just have a method that re-calculates the height, saves it to the array, and updates the table. Something like:
CGSize constraintSize;
constraintSize.width = 290.0f;
constraintSize.height = MAXFLOAT;
NSString *text = #"YOUR TEXT"
CGSize theSize = [text sizeWithFont:[UIFont systemFontOfSize:15.0f] constrainedToSize:constraintSize lineBreakMode:UILineBreakModeWordWrap];
NSLog(#"height: %f",theSize.height);
will give you the height.
This configuration should give you something simillar to what you see when you enter a loooong number in the phone app -
label.minimumFontSize = 4; //a very small font size
label.adjustsFontSizeToFitWidth = YES;
label.lineBreakMode = UILineBreakModeWordWrap;// change to what works for you
label.numberOfLines = 0;
See lineBreakMode Documentation

NSView resizing

I have an NSView with a NSTextField in it. The problem is, each time before displaying the view, the size of the NSTextField can change and I want the size of NSView to change with it. So if textfield is 2 lines long, the nsview will be small (just barely surround the textfield) but when its 10 lines long, I don't want the view's size to cut off the textfield, i want the view to grow with it.
How can I do that? Thanks.
This is what I use in my tableview, but it should apply equally to your situation:
float textLabelWidth = 190;
NSString *Text = [[deals objectAtIndex:[indexPath section]] objectForKey:#"title"];
UIFont *cellFont = [UIFont fontWithName:#"Helvetica" size:13.0];
CGSize constraintSize = CGSizeMake(textLabelWidth, MAXFLOAT);
CGSize labelSize = [Text sizeWithFont:cellFont constrainedToSize:constraintSize lineBreakMode:UILineBreakModeWordWrap];
return labelSize.height + 20; // <--- this is what your NSView's height should be, just edit the textLabelWidth and padding (in this case 20) to whatever you desire.
Hope that helps!
Your need to set the "contentHugging and contentCompression" priorities.
In short:
contentHugging: sets the preference where a control resists being made *larger* than it's intrinsic size
contentCompression: sets the preference where a control resists being made *smaller* than it's intrinsic size
Apple have a reference here:
https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithConstraintsinInterfaceBuidler.html
Specifically scroll down to the Setting Content-Hugging and Compression-Resistance Priorities section.
This is what deals with textFields etc... clipping (i.e. when a textfield has "some testValue" becomes "some testV..." etc...