uilabel tail truncation - objective-c

Im working on an ios app using objective c and i have an issue with uilabel that i could use some help with. Basically i have a label that can change size to fit the text that it will display but it has a max height that it can possible be. the label itself has a fixed width at all times. i have turned on UILineBreakModeWordWrap and UILineBreakModeTailTruncation to make the text fit and truncate but this causes the text to truncate the tail too early when it has only 1 word left to place. rather then moving it onto the next line when there is still room it just truncates it.
self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, fixedWidth, 0);
self.lineBreakMode = UILineBreakModeWordWrap | UILineBreakModeTailTruncation;
self.numberOfLines = 0;
[self sizeToFit];
is there anyway of finding when the uilabel is actually truncating the text so i can then check the label height and add to it if there is still room ? I tried always adding an extra line to the height when there is room and this avoids the early truncation but then im left with inconsistent sizing of the over all label. any ideas on this would be great thanks

lineBreakMode is a switch. It can be either (for iOS6+) NSLineBreakByWordWrapping or NSLineBreakByTruncatingTail but not both.
But, to answer your question, you can find the size of some text using the class extensions in NSString+UIKit. Having found the size you could update the frame of the UILabel appropriately.

Using this method:
How to find UILabel's number of Lines
You could set the label to the max height, find out how tall the text is in that label and shrink it as necessary.

I have written a category for working with UILabel's truncation. Works on iOS 7 and later. Hope it helps !
#implementation UILabel (Truncation)
- (NSRange)truncatedRange
{
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:[self attributedText]];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager:layoutManager];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:[self bounds].size];
textContainer.lineFragmentPadding = 0;
[layoutManager addTextContainer:textContainer];
NSRange truncatedrange = [layoutManager truncatedGlyphRangeInLineFragmentForGlyphAtIndex:0];
return truncatedrange;
}
- (BOOL)isTruncated
{
return [self truncatedRange].location != NSNotFound;
}
- (NSString *)truncatedText
{
NSRange truncatedrange = [self truncatedRange];
if (truncatedrange.location != NSNotFound)
{
return [self.text substringWithRange:truncatedrange];
}
return nil;
}
#end

I just came to a similar problem, and solved it with a very simple solution (tested on ios 8.4, xcode 7).
In IB (I use autolayout with some constraints):
set UIlabel numberOfLine = 1;
LineBrakeMode = TruncateTail
In Code:
label.numberOfLines = 2; // I can only display 2 row.
// I supposed you should know how many rows you can displayed.
label.preferredMaxLayoutWidth = label.frame.size.width;
[label sizeToFit];
Tadaa. That's it. Note that this only work with UILabel. With UIButton, it may not work (Haven't tested).

Related

How to check if NSTextfield is already truncating the text (... at the end)

I've searched around on how to perform this but I can't find any answer.
I'd like to know if my NSTextfield is already truncating the text (... at the end) without having to check the length of its stringValue. I need to do this to know if I should set a tooltip on my NSTextfield (acting like a simple label).
The reason I don't want to check the length of my textfield's stringValue it's because there are some characters that occupy more space than others, so that's not very accurate
Thanks!
For future visitors, you can use the -[NSCell expansionFrameWithFrame:inView:] method to determine if truncation is taking place. From the header:
Allows the cell to return an expansion cell frame if cellFrame is
too small for the entire contents in the view. ...<snip>... If the frame is not too
small, return an empty rect, and no expansion tool tip view will be
shown. By default, NSCell returns NSZeroRect, while some subclasses
(such as NSTextFieldCell) will return the proper frame when required.
In short, you can tell if an NSTextField is truncating like this:
NSRect expansionRect = [[self cell] expansionFrameWithFrame: self.frame inView: self];
BOOL truncating = !NSEqualRects(NSZeroRect, expansionRect);
Your best bet might be to use an NSTextView instead of a NSTextField. If you use an NSTextView you can get the NSTextContainer of the NSTextView using the textContainer property. The container can tell you the containerSize (the space the text is drawn in).
NSString objects respond to the method sizeWithAttributes:. You can use the resulting NSSize struct to grab the width of the text if drawn with the given attributes. See the "Constants" section of the NSAttributedString Application Kit Additions Reference to figure out what attributes are relevant.
If the containerSize width is less than the sizeWithAttributes: width then the text will be truncated.
EDIT: Apologies, this is only true with no lineFragmentPadding, but the default lineFragmentPadding is non-zero. Either subtract the textContainer.lineFragmentPadding from containerSize.width or use the NSTextContainer setLineFragmentPadding: method to set it to 0.
I suppose you could also make some assumptions about the text area relative to the size of the NSTextField and use the sizeWithAttributes: NSString method in conjunction with that, but that is not as clean.
EDIT 2: I realized I did not address the OP's interest in truncating the text using ellipses. The example code below uses truncation in the NSTextView. I also thought I might as well throw in some code that makes the NSTextView a little more similar in appearance to the NSTextField by putting it inside of a NSBox. Adding a check for size to determine if a tooltip should be displayed would be a simple addition to the code below using the information already mentioned above.
NSString* string = #"Hello World.";
// get the size the text will take up using the default font
NSSize textBounds = [string sizeWithAttributes:#{}];
// Create a border view that looks more or less like the border used around
// text fields
NSBox* borderView = [[NSBox alloc] initWithFrame:NSMakeRect(10, 10, 60, textBounds.height+4)];
[borderView setBoxType:NSBoxCustom];
[borderView setBorderType:NSBezelBorder];
[borderView setContentViewMargins:NSMakeSize(0, 0)];
[borderView setFillColor:[NSColor whiteColor]];
// Create the text view
NSTextView* textView = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, 60, textBounds.height)];
[textView setTextContainerInset:NSMakeSize(2, 0)];
[textView.textContainer setLineFragmentPadding:0];
[textView setEditable:YES];
// Set the default paragraph style so the text is truncated rather than
// wrapped
NSMutableParagraphStyle* parStyle = [[NSMutableParagraphStyle alloc] init];
[parStyle setLineBreakMode:NSLineBreakByTruncatingTail];
// Do not let text get squashed to fit
[parStyle setTighteningFactorForTruncation:0.0];
[textView setDefaultParagraphStyle:parStyle];
[parStyle release];
// Set text
[textView setString:string];
// add NSTextView to border view
[borderView addSubview:textView];
[textView release];
// add NSBox to view you want text view displayed in
[self addSubview:borderView];
[borderView release];
I think you can do it by looking at the frame of the text field's field editor. You check its width in controlTextDidBeginEditing, and then again in controlTextDidEndEditing. If the latter value is larger, then the text has been truncated. The following is implemented in the text field's delegate (I created an ivar for initialWidth):
- (void)controlTextDidBeginEditing:(NSNotification *)aNotification {
if (! initialWidth)
initialWidth = ((NSTextView *)aNotification.userInfo[#"NSFieldEditor"]).frame.size.width;
}
- (void)controlTextDidEndEditing:(NSNotification *)aNotification {
if (initialWidth) { //Make sure that beginEditing is called first
NSInteger finalWidth = ((NSTextView *)aNotification.userInfo[#"NSFieldEditor"]).frame.size.width;
if (finalWidth - initialWidth > 1) NSLog(#"We have truncation");
NSLog(#"Final: %ld Initial: %ld", finalWidth,initialWidth);
}
}
This seems to work most of the time, including if you type in a long string, then delete until it fits again. In a few cases, it gave me the log message when I was one character past the end of the text field, but I did not get the ellipsis when editing ended.
Swift 4 solution using ipmcc as base
It will resize one by one until it fit or fontsize = 3
var size_not_ok = true
var conter = 0
let mininum_font_size = 3 // will resize ultil 3
while size_not_ok || conter < 15 { // will try 15 times maximun
let expansionRect = le_nstextfield.expansionFrame(withFrame: le_nstextfield.frame)
let truncated = !NSEqualRects(NSRect.zero, expansionRect)
if truncated {
if let actual_font_size : CGFloat = le_nstextfield.font?.fontDescriptor.object(forKey: NSFontDescriptor.AttributeName.size) as? CGFloat {
le_nstextfield.font = NSFont.systemFont(ofSize: actual_font_size - 1)
if actual_font_size < mininum_font_size {
break
}
}
} else {
size_not_ok = false
}
conter = conter + 1
}

boundingRectWithSize for NSAttributedString returning wrong size

I am trying to get the rect for an attributed string, but the boundingRectWithSize call is not respecting the size I pass in and is returning a rect with a single line height as opposed to a large height (it is a long string). I have experimented by passing in a very large value for the height and also 0 as in the code below, but the rect returned is always the same.
CGRect paragraphRect = [attributedText boundingRectWithSize:CGSizeMake(300,0.0)
options:NSStringDrawingUsesDeviceMetrics
context:nil];
Is this broken, or do I need to do something else to have it returned a rect for wrapped text?
Looks like you weren't providing the correct options. For wrapping labels, provide at least:
CGRect paragraphRect =
[attributedText boundingRectWithSize:CGSizeMake(300.f, CGFLOAT_MAX)
options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading)
context:nil];
Note: if the original text width is under 300.f there won't be line wrapping, so make sure the bound size is correct, otherwise you will still get wrong results.
For some reason, boundingRectWithSize always returns wrong size.
I figured out a solution.
There is a method for UItextView -sizeThatFits which returns the proper size for the text set.
So instead of using boundingRectWithSize, create an UITextView, with a random frame, and call its sizeThatFits with the respective width and CGFLOAT_MAX height.
It returns the size that will have the proper height.
UITextView *view=[[UITextView alloc] initWithFrame:CGRectMake(0, 0, width, 10)];
view.text=text;
CGSize size=[view sizeThatFits:CGSizeMake(width, CGFLOAT_MAX)];
height=size.height;
If you are calculating the size in a while loop, do no forget to add that in an autorelease pool, as there will be n number of UITextView created, the run time memory of the app will increase if we do not use autoreleasepool.
Ed McManus has certainly provided a key to getting this to work. I found a case that does not work
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 will not have the correct height. Notice that anotherString (which is appended to string) was initialized without an attribute dictionary. This is a legitimate initializer for anotherString but boundingRectWithSize: does not give an accurate size in this case.
My final decision after long investigation:
- boundingRectWithSize function returns correct size for uninterrupted sequence of characters only!
In case string contains spaces or something else (called by Apple "Some of the glyphs" ) - it is impossible to get actual size of rect needed to display text!
I have replaced spaces in my strings by letters and immediately got correct result.
Apple says here:
https://developer.apple.com/documentation/foundation/nsstring/1524729-boundingrectwithsize
"This method returns the actual bounds of the glyphs in the string. Some of the glyphs (spaces, for example) are allowed to overlap the layout constraints specified by the size passed in, so in some cases the width value of the size component of the returned CGRect can exceed the width value of the size parameter."
So it is necessary to find some another way to calculate actual rect...
After long investigation process solution finally found!!!
I am not sure it will work good for all cases related to UITextView, but main and important thing was detected!
boundingRectWithSize function as well as CTFramesetterSuggestFrameSizeWithConstraints (and many other methods) will calculate size and text portion correct when correct rectangle used.
For example - UITextView has textView.bounds.size.width - and this value not actual rectangle used by system when text drawing on UITextView.
I found very interesting parameter and performed simple calculation in code:
CGFloat padding = textView.textContainer.lineFragmentPadding;
CGFloat actualPageWidth = textView.bounds.size.width - padding * 2;
And magic works - all my texts calculated correct now!
Enjoy!
Swift four version
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
Measuring the text with the CTFramesetter works best as it provides integer sizes and handles emoji's and other unicode characters well.
I didn't have luck with any of these suggestions. My string contained unicode bullet points and I suspect they were causing grief in the calculation. I noticed UITextView was handling the drawing fine, so I looked to that to leverage its calculation. I did the following, which is probably not as optimal as the NSString drawing methods, but at least it's accurate. It's also slightly more optimal than initialising a UITextView just to call -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);
Turns out that EVERY part of an NSAttributedString must have a dictionary set with at least NSFontAttributeName and NSForegroundColorAttributeName set, if you wish boundingRectWithSize to actually work!
I don't see that documented anywhere.
In case you'd like to get bounding box by truncating the tail, this question can help you out.
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];
I've found that the preferred solution does not handle line breaks.
I've found this approach works in all cases:
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;
#warrenm Sorry to say that framesetter method didn't work for me.
I got this.This function can help us to determine the frame size needed for a string range of an NSAttributedString in iphone/Ipad SDK for a given Width :
It can be used for a dynamic height of UITableView Cells
- (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));
}
Thanks to HADDAD ISSA >>> http://haddadissa.blogspot.in/2010/09/compute-needed-heigh-for-fixed-width-of.html
I have had the same problem with not getting an accurate size using these techniques and I've changed my approach to make it work.
I have a long attributed string which I've been trying to fit into a scroll view so that it shows properly without being truncated. What I did to make the text work reliably was to not set the height at all as a constraint and instead allowed the intrinsic size to take over. Now the text is displayed correctly without being truncated and I do not have to calculate the height.
I suppose if I did need to get the height reliably I would create a view which is hidden and these constraints and get the height of the frame once the constraints are applied.
Update July 2022
After many more trial and error and getting feedback from other answers, specifically the ones pointing out to use NSString.DrawingOptions.usesDeviceMetrics, I found out that this option is definitely a game changer, though is not enough on itself.
Using .deviceMetrics returns the correct height, but it won't fit properly on a UILabel nor on a NSTextField on some cases.
The only way I was able to make it fit on all cases was using a CATextLayer. Which is available for both iOS and macOS.
Example
let attributedString = NSAttributedString(string: "my string")
let maxWidth = CGFloat(300)
let size = attributedString.boundingRect(
with: .init(width: maxWidth,
height: .greatestFiniteMagnitude),
options: [
.usesFontLeading,
.usesLineFragmentOrigin,
.usesDeviceMetrics])
let textLayer = CATextLayer()
textLayer.frame = .init(origin: .zero, size: size)
textLayer.contentsScale = 2 // for retina
textLayer.isWrapped = true // for multiple lines
textLayer.string = attributedString
Then you can add the CATextLayer to any NSView/UIView.
macOS
let view = NSView()
view.wantsLayer = true
view.layer?.addSublayer(textLayer)
iOS
let view = UIView()
view.layer.addSublayer(textLayer)
Original answer February 2021
Many of the answers here are great, David Rees summarises the options nicely.
But sometimes when there are special characters or multiple white spaces the size seemed to always be wrong.
Example of a not working string (for me):
"hello . . world"
What I found out is that setting the kern of the NSAttributedString to 1 helps returning the right size.
Like this:
NSAttributedString(
string: "some string",
attributes: [
.font: NSFont.preferredFont(forTextStyle: .body),
.kern: 1])
Im a little late to the game - but I have been trying to figure out a way that works to find the bounding box that will fit around an attributed string to make a focus ring like editing a file in Finder does. everything I had tried failed when there are spaces at the end of the string or multiple spaces inside the string. boundingRectWithSize fails miserably for this as well as CTFramesetterCreateWithAttributedString.
Using a NSLayoutManager the following code seems to do the trick in all the cases I have found so far and returns a rect that perfectly bounds the string. Bonus: if you select the text the edges of the selection go right up to the bounds of the rect returned. The code below uses the layoutManager from a NSTextView.
NSLayoutManager* layout = [self layoutManager];
NSTextContainer* container = [self textContainer];
CGRect focusRingFrame = [layout boundingRectForGlyphRange:NSMakeRange(0, [[self textStorage] length]) inTextContainer:container];
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));
Works on all fonts perfectly!
I had the same problem, but I recognised that height constrained has been set correctly. So I did the following:
-(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;
}
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);
I tried everything on this page and still had one case for a UILabel that was not formatting correctly. Actually setting the attributedText on the label finally fixed the problem.
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];
One thing I was noticing is that the rect that would come back from (CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options attributes:(NSDictionary *)attributes context:(NSStringDrawingContext *)context would have a larger width than what I passed in. When this happened my string would be truncated. I resolved it like this:
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;
For some more context; I had multi line text and I was trying to find the right height to display it in. boundRectWithSize was sometimes returning a width larger than what I would specify, thus when I used my past in width and the calculated height to display my text, it would truncate. From testing when boundingRectWithSize used the wrong width the amount it would make the height short by was 1 line. So I would check if the width was greater and if so add the font's lineHeight to provide enough space to avoid truncation.
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;
if label's frame not add 10 this method will never work! hope this can help you! goog luck.
I'd like to add my thoughts since I had exactly the same problem.
I was using UITextView since it had nicer text alignment (justify, which at the time was not available in UILabel), but in order to "simulate" non-interactive-non-scrollable UILabel, I'd switch off completely scrolling, bouncing, and user interaction.
Of course, problem was that text was dynamic, and while width would be fixed, height should be recalculated every time I'd set new text value.
boundingRectWithSize didn't work well for me at all, from what I could see, UITextView was adding some margin on top which boundingRectWithSize would not get into a count, hence, height retrieved from boundingRectWithSize was smaller than it should be.
Since text was not to be updated rapidly, it's just used for some information that may update every 2-3 seconds the most, I've decided following approach:
/* 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 );
}
*Above code is not directly copied from my source, I had to adjust it / clear it from bunch of other stuff not needed for this article. Don't take it for copy-paste-and-it-will-work-code.
Obvious disadvantage is that it has alloc and release, for each call.
But, advantage is that you avoid depending on compatibility between how boundingRectWithSize draws text and calculates it's size and implementation of text drawing in UITextView (or UILabel which also you can use just replace UITextView with UILabel). Any "bugs" that Apple may have are this way avoided.
P.S. It would seem that you shouldn't need this "temp" UITextView and can just ask sizeThatFits directly from target, however that didn't work for me. Though logic would say it should work and alloc/release of temporary UITextView are not needed, it did not. But this solution worked flawlessly for any text I would set in.
Ok so I spent lots of time debugging this. I found out that the maximum text height as defined by boundingRectWithSize allowed to display text by my UITextView was lower than the frame size.
In my case the frame is at most 140pt but the UITextView tolerate texts at most 131pt.
I had to figure that out manually and hardcode the "real" maximum height.
Here is my 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;
}
Encountered exactly same issue.
To me, the issue is solved by TTTAttributedLabel's
+ (CGSize)sizeThatFitsAttributedString:(NSAttributedString *)attributedString
withConstraints:(CGSize)size
limitedToNumberOfLines:(NSUInteger)numberOfLines
method, as it provide accurate result.
I had issues calculating the height of an NSTextField. Any method I tried was always returning values that were too small.
For me the problem turned out to be that, for some reason, NSTextField's attributedStringValue property never contained any of the attributes that I set via Interface Builder. It actually contained no attributes at all if I didn't set an attributed string programmatically. Not even a font. That is why all of the height calculations were botched.
To get it to work, I created a Category for NSTextField which implements a custom function for getting the correct attributed string.
Here's the implementation file for that Category:
//
// --------------------------------------------------------------------------
// NSTextField+Additions.m
// Created for Mac Mouse Fix (https://github.com/noah-nuebling/mac-mouse-fix)
// Created by Noah Nuebling in 2021
// Licensed under MIT
// --------------------------------------------------------------------------
//
#import "NSTextField+Additions.h"
#implementation NSTextField (Additions)
// Copy paste template for adding attributes to an attributed string. Contains all possible attributes
// [str addAttributes:#{
// NSFontAttributeName: NSNull.null,
// NSParagraphStyleAttributeName: NSNull.null,
// NSForegroundColorAttributeName: NSNull.null,
// NSBackgroundColorAttributeName: NSNull.null,
// NSLigatureAttributeName: NSNull.null,
// NSKernAttributeName: NSNull.null,
// NSStrikethroughStyleAttributeName: NSNull.null,
// NSUnderlineStyleAttributeName: NSNull.null,
// NSStrokeColorAttributeName: NSNull.null,
// NSStrokeWidthAttributeName: NSNull.null,
// NSShadowAttributeName: NSNull.null,
// NSTextEffectAttributeName: NSNull.null,
// NSAttachmentAttributeName: NSNull.null,
// NSLinkAttributeName: NSNull.null,
// NSBaselineOffsetAttributeName: NSNull.null,
// NSUnderlineColorAttributeName: NSNull.null,
// NSStrikethroughColorAttributeName: NSNull.null,
// NSObliquenessAttributeName: NSNull.null,
// NSExpansionAttributeName: NSNull.null,
// NSWritingDirectionAttributeName: NSNull.null,
// NSVerticalGlyphFormAttributeName: NSNull.null,
// } range:NSMakeRange(0, str.length)];
/// In my testing NSTextField.attributedStringValue actually returned a string without _any_ attributes. Not even a font or anything.
/// This lead to issues when trying to calculate the fitting height for a certain width of the NSTextField.
/// This function takes some of the properties of the NSTextField and returns an NSAttributed string based on those.
/// I'm not sure this is perfect, but the returned attributed string describes the way that the text of the NSTextField is rendered close enough to be usable for my height calculations
- (NSAttributedString *)effectiveAttributedStringValue {
NSMutableAttributedString *str = self.attributedStringValue.mutableCopy;
// Create paragraph style from NSTextField properties
// Not sure if we're setting these properties correctly, and there could be more properties we should be setting
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.alignment = self.alignment;
paragraphStyle.baseWritingDirection = self.baseWritingDirection;
paragraphStyle.lineBreakMode = self.lineBreakMode;
paragraphStyle.allowsDefaultTighteningForTruncation = self.allowsDefaultTighteningForTruncation;
if (#available(macOS 10.15, *)) paragraphStyle.lineBreakStrategy = self.lineBreakStrategy;
// Add attributes to AttributedString based on NSTextField properties
[str addAttributes:#{
NSFontAttributeName: self.font,
NSParagraphStyleAttributeName: paragraphStyle,
NSForegroundColorAttributeName: self.textColor,
NSBackgroundColorAttributeName: self.backgroundColor,
// NSLigatureAttributeName: NSNull.null,
// NSKernAttributeName: NSNull.null,
// NSStrikethroughStyleAttributeName: NSNull.null,
// NSUnderlineStyleAttributeName: NSNull.null,
// NSStrokeColorAttributeName: NSNull.null,
// NSStrokeWidthAttributeName: NSNull.null,
// NSShadowAttributeName: NSNull.null, //self.shadow,
// NSTextEffectAttributeName: NSNull.null,
// NSAttachmentAttributeName: NSNull.null,
// NSLinkAttributeName: NSNull.null,
// NSBaselineOffsetAttributeName: NSNull.null, //self.baselineOffsetFromBottom,
// NSUnderlineColorAttributeName: NSNull.null,
// NSStrikethroughColorAttributeName: NSNull.null,
// NSObliquenessAttributeName: NSNull.null,
// NSExpansionAttributeName: NSNull.null,
// NSWritingDirectionAttributeName: NSNull.null, //self.baseWritingDirection,
// NSVerticalGlyphFormAttributeName: NSNull.null,
} range:NSMakeRange(0, str.length)];
// return NSAttributedString
return str;
}
#end
Random Sidenotes
Some of the issues I've read about people having with UILabel in this thread sound a lot like they might be related.
I eventually decided to use NSTextView over NSTextField because its methods for obtaining the attributed string work out of the box, and using NSTextField for clickable links was completely botched as well. I'm under the impression that NSTextField is just a buggy mess that you should avoid beyond the most basic of use-cases.
I was having issues sometimes calculating some heights with boundingRect, specially with paragraphs and break lines. Adding .usesDeviceMetrics as a parameter did the trick. Now seems to work fine in all cases.
extension NSAttributedString {
func heightWithWidth(_ width: CGFloat) -> CGFloat {
let constraints = CGSize(width: width, height: .infinity)
let bounding = self.boundingRect(with: constraints, options: [.usesLineFragmentOrigin, .usesFontLeading, .usesDeviceMetrics], context: nil)
return bounding.height
}
}

UISegmentedControl with Bezeled Style uncenters titles on Device

for clarification I'll just add 2 overlaid Screenshots, one in Interface Builder, the other on the device.
The lower UISegmentedControl is fresh out of the library with no properties edited, still it looks different on the Device (in this case a non-Retina iPad, though the problem is the same for Retina-iPhone) (Sorry for the quick and dirty photoshopping)
Any ideas?
EDIT: I obviously tried the "alignment" under "Control" in the Utilities-Tab in Interface Builder. Unfortunately none of the settings changed anything for the titles in the UISegment. I don't think they should as they are not changing titles in Interface Builder either.
EDIT2: Programmatically setting:
eyeSeg.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
doesn't make a difference either.
Found the Problem "UISegmentedControlStyleBezeled is deprecated. Please use a different style."
See also what-should-i-use-instead-of-the-deprecated-uisegmentedcontrolstylebezeled-in-io
Hmm...have you checked the alignment? Maybe that's the case.
You can recursively search the subviews of the UISegmentedControl view for each of the UILabels in the segmented control and then change the properties of each UILabel including the textAlignment property as I've shown in a sample of my code. Credit to Primc's post in response to Change font size of UISegmented Control for suggesting this general approach to customizing the UILabels of a UISegmentedControl. I had been using this code with the UISegmentedControlStyleBezeled style by the way even after it was deprecated although I have recently switched to UISegmentedControlStyleBar with an adjusted frame height.
- (void)viewDidLoad {
[super viewDidLoad];
// Adjust the segment widths to fit the text. (Will need to calculate widths if localized text is ever used.)
[aspirationControl setWidth:66 forSegmentAtIndex:0]; // Navel Lint Collector
[aspirationControl setWidth:48 forSegmentAtIndex:1]; // Deep Thinker
[aspirationControl setWidth:49 forSegmentAtIndex:2]; // Mental Wizard
[aspirationControl setWidth:64 forSegmentAtIndex:3]; // Brilliant Professor
[aspirationControl setWidth:58 forSegmentAtIndex:4]; // Nobel Laureate
// Reduce the font size of the segmented aspiration control
[self adjustSegmentText:aspirationControl];
}
- (void)adjustSegmentText:(UIView*)view {
// A recursively called method for finding the subviews containing the segment text and adjusting frame size, text justification, word wrap and font size
NSArray *views = [view subviews];
int numSubviews = views.count;
for (int i=0; i<numSubviews; i++) {
UIView *thisView = [views objectAtIndex:i];
// Typecast thisView to see if it is a UILabel from one of the segment controls
UILabel *tmpLabel = (UILabel *) thisView;
if ([tmpLabel respondsToSelector:#selector(text)]) {
// Enlarge frame. Segments are set wider and narrower to accomodate the text.
CGRect segmentFrame = [tmpLabel frame];
// The following origin values were necessary to avoid text movement upon making an initial selection but became unnecessary after switching to a bar style segmented control
// segmentFrame.origin.x = 1;
// segmentFrame.origin.y = -1;
segmentFrame.size.height = 40;
// Frame widths are set equal to 2 points less than segment widths set in viewDidLoad
if ([[tmpLabel text] isEqualToString:#"Navel Lint Collector"]) {
segmentFrame.size.width = 64;
}
else if([[tmpLabel text] isEqualToString:#"Deep Thinker"]) {
segmentFrame.size.width = 46;
}
else if([[tmpLabel text] isEqualToString:#"Mental Wizard"]) {
segmentFrame.size.width = 47;
}
else if([[tmpLabel text] isEqualToString:#"Brilliant Professor"]) {
segmentFrame.size.width = 62;
}
else {
// #"Nobel Laureate"
segmentFrame.size.width = 56;
}
[tmpLabel setFrame:segmentFrame];
[tmpLabel setNumberOfLines:0]; // Change from the default of 1 line to 0 meaning use as many lines as needed
[tmpLabel setTextAlignment:UITextAlignmentCenter];
[tmpLabel setFont:[UIFont boldSystemFontOfSize:12]];
[tmpLabel setLineBreakMode:UILineBreakModeWordWrap];
}
if (thisView.subviews.count) {
[self adjustSegmentText:thisView];
}
}
}
The segmented control label text has an ugly appearance in IB but comes out perfectly centered and wrapped across 2 lines on the device and in the simulator using the above code.

NSTextField with autosizing text looks terrible when in an NSCollectionView

I am having a little problem here. I have an NSCollectionView with a collection of views which have an NSTextField that shows fontnames in those fonts. Here is a screenshot:
As you can see, some fonts look glitched, they have their Interface Builder font-size behind them. I use this code to set the font size in initWithFrame: of the NSTextField:
float targetWidth = rect.size.width - 10;
float targetHeight = rect.size.height - 10;
int i;
for (i = 10; i < 100; i++) {
NSDictionary *attrs = [[NSDictionary alloc] initWithObjectsAndKeys:[NSFont fontWithName:[self.font fontName] size:i], NSFontAttributeName, nil];
NSSize strSize = [[self stringValue] sizeWithAttributes:attrs];
[attrs release];
if (strSize.width > targetWidth || strSize.height > targetHeight) {
break;
}
}
[self setFont:[NSFont fontWithName:[self.font fontName] size:(i - 1)]];
How can I fix this? It's looking terrible. Thanks in advance.
This almost seems like it's more a problem with how the collection view is animated. Have you turned on wantsLayers for your prototype collection item view? Try turning it off. Also try putting an opaque view behind the text field so there's no transparency behind it all the way through to the collection view itself and see that improves things.
If that doesn't work, please clarify why and exactly how you're using the -initWithFrame: method of the text field ... seems to be a strange way of doing things.

Sizing a UILabel to fit?

How would one modify the following snippet (in a tableView:cellForRowAtIndexPath: UITableViewController method) from the "09a - PrefsTable" recipe from Chapter 6 of The iPhone Developer's Cookbook:
if (row == 1) {
// Create a big word-wrapped UILabel
cell = [tableView dequeueReusableCellWithIdentifier:#"libertyCell"];
if (!cell) {
cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:#"libertyCell"] autorelease];
[cell addSubview:[[UILabel alloc] initWithFrame:CGRectMake(20.0f, 10.0f, 280.0f, 330.0f)]];
}
UILabel *sv = [[cell subviews] lastObject];
sv.text =  #"When in the Course of human events, it becomes necessary for one people to dissolve the political bands which have connected them with another, and to assume among the powers of the earth, the separate and equal station to which the Laws of Nature and of Nature's God entitle them, a decent respect to the opinions of mankind requires that they should declare the causes which impel them to the separation.";
sv.textAlignment = UITextAlignmentCenter;
sv.lineBreakMode = UILineBreakModeWordWrap;
sv.numberOfLines = 9999;
return cell;
}
...to size the "sv" UILabel subview and the "cell" UITableViewCell to be sized just big enough to fit the text (and work with more or less text, and other types of text alignment)?  I looked at the UILabel textRectForBounds:limitedToNumberOfLines: method, but the documentation states that it should not be called directly (and should only be overridden).  I experimented with the UIView sizeToFit method, without success.
Update: I asked a new question about my problem with the NSString -sizeWithFont:forWidth:lineBreakMode: method.
I had to do this enough that I extended UILabel to do it for me:
#interface UILabel (BPExtensions)
- (void)sizeToFitFixedWidth:(CGFloat)fixedWidth;
#end
#implementation UILabel (BPExtensions)
- (void)sizeToFitFixedWidth:(CGFloat)fixedWidth
{
self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, fixedWidth, 0);
self.lineBreakMode = NSLineBreakByWordWrapping;
self.numberOfLines = 0;
[self sizeToFit];
}
#end
then to have a label to have a variable multiline height but a fixed width just:
[myLabel sizeToFitFixedWidth:kSomeFixedWidth];
You should use NSString's -sizeWithFont:forWidth:lineBreakMode: method to retrieve the associated sizing metrics for your label.
Also, change the numberOfLines property to 0 if you're going to use that code.
NSString's -sizeWithFont:forWidth:lineBreakMode: does not actually perform the word wrap. Instead, use -sizeWithFont:constrainedToSize:lineBreakMode: to get an accurate width AND height value for the string.
Try this:
sv.text = #"When in the Course of human events, it becomes necessary for one people to dissolve the political bands which have connected them with another, and to assume among the powers of the earth, the separate and equal station to which the Laws of Nature and of Nature's God entitle them, a decent respect to the opinions of mankind requires that they should declare the causes which impel them to the separation.";
sv.textAlignment = UITextAlignmentCenter;
sv.lineBreakMode = UILineBreakModeWordWrap;
sv.numberOfLines = 0;
[sv sizeToFit];
Also, you will need to implement the UITableViewDelegate method:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
And have it return a total cell height adjusted for the resized text field.
One other note - Size to Fit should actually work, if you have number of lines set to 0 as previously mentioned. It would give you back a size with the height increased to accomidate the word-wrapped text set in the label and the width set to whatever the original label width had.
That will not help you though as you need to get the size in heightForRow before the cell is obtained, so you are better off calculating the height needed (and very probably caching that calculation so as not to slow down table rendering)
Here's a bit of code i use:
CGSize textSize = [myLabel.text sizeWithFont:myLabel.font];
I had similar problem, I had a UITableViewCell that was designed in StoryBoards as a static cell. I used [super tableView:cellForRowAtIndexPath:] to get it. So I wanted to resize the UILabel "detailTextLabel" so it fits the text I set to it. The style was "Right Detail".
I just set the text in my tableView:cellForRowAtIndexPath:. And than in tableView:heightForRowAtIndexPath: I returned
UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath];
return cell.detailTextLabel.frame.size.height
I had long string. And finally had a wide Cell with 4 lines of text in label.
I had similar problem.
I solved this.
In cellForRowAtIndexPath method set font size to whatever you want.
cell.textLabel.lineBreakMode = UILineBreakModeWordWrap;
cell.textLabel.numberOfLines = 0;
[cell.textLabel setFont:[UIFont systemFontOfSize:14.0]];
[cell.textLabel sizeToFit];
And in heightForRowAtIndexPath method increase font size.
CGFloat height;
UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath];
NSString *text = cell.detailTextLabel.text;
CGSize constraint = CGSizeMake(320, 20000.0f);
CGSize size = [text sizeWithFont:[UIFont systemFontOfSize:20.0] constrainedToSize:constraint lineBreakMode:UILineBreakModeWordWrap];
CGFloat calHeight = MAX(size.height, 44.0f);
height = calHeight + (CELL_CONTENT_MARGIN * 2);
return height;