Ignore Ascender and Descender when centering UILabel vertically? - ios

I’m using AutoLayout to position some labels in the vertical centre of a cell. The text is in all-caps, but the UILabel in question, even when sizeToFit is applied, leaves space below the text, which looks a lot like it would be for the tails on letters such as a lower case y, p, and q. Since I’m centring vertically, this is causing an offset and meaning the text appears a few pixels higher than it should do.
Another question may be: can I have a font intelligently adjust its vertical centre dependant on whether it contains any characters which use the ascender or descender?
For instance, the string “abbaba” doesn’t need the descender, whereas the string “oyyoyo” doesn’t need the ascender. Strings in all-caps also never need the descender. If I vertically center “oyyoyoyo” it’ll appear too low.

Thanks, Abhinit, for your answer.
I was also looking for this so I would like to post here the exact constraints you need to apply to align texts to your liking.
This image from Wikipedia shows the different size sections of a font.
So, there are many ways to align a label depending on whether you want to align to the ascender height, the cap height, the x-height, baseline or descender height.
Let's say you have a label containing text in caps like "HELLO" and you want to align with viewAbove to cap height and align with viewBelow to baseline.
You would do:
let font = label.font
let ascenderDelta = font.ascender - font.capHeight
LayoutHelper()
.addViews([
"label":label, "viewAbove":viewAbove, "viewBelow":viewBelow
])
.withMetrics(["ascenderDelta":ascenderDelta])
.addConstraints([
// -- Here the constraints to align to cap height --
"X:label.top == viewAbove.bottom - ascenderDelta",
"X:label.baseline == viewBelow.top",
...other constraints...
])
Note: in the example I'm using my utility class LayoutHelper, but I hope the idea is clear.
About an "auto-aligning" label:
I will think about making an "intelligent" label that adjusts to the appropriate line depending on whether it contains descenders, ascenders, caps, etc.
You could do it using negative insets in drawTextInRect(like here but, for example, using insets = {-ascenderDelta, 0, font.descender, 0}). But that would crop any ascenders/descenders in case you had. I would prefer to align to caps without cropping any possible ascender.

You can use the 'capHeight' and 'xHeight' properties on UIFont to get the correct height and use that to size the UILabel.
Of course this assumes that you know for sure if a string would be lowercase or uppercase only. If not, then you can override setText on a UILabel and check every time the function gets called.
I would also think of looking deeper into CoreText and implementing something like this http://www.zsiegel.com/2012/10/23/Core-Text-Calculating-line-heights/

I had the same problem and solved it by subclassing UILabel and changing its draw method:
- (void)drawRect:(CGRect)rect
{
CGRect capCenteredRect = CGRectMake(rect.origin.x, (self.font.leading-self.font.capHeight)*0.5f, rect.size.width, rect.size.height);
[super drawTextInRect:capCenteredRect];
}
In my case I needed to center to caps because the vertical centering of the UILabel is always some pixels off.
If you need to center vertically to lower case letters with descender you can change the rect to:
CGRectMake(rect.origin.x, (self.font.leading-self.font.xHeight)*0.5f+self.font.descender, rect.size.width, rect.size.height)

Related

How can I set the height of a UITextView to exactly match its truncated content?

I have a UITextView whose height I would like to limit to some reasonable value, with the text truncating if necessary. How can I make sure that the height of the text view matches that of the truncated content? If, for example, I set the height to a fixed value, there will some variable space at the bottom of the text view which will affect the layout of items below it.
Is there some way to set a desired height, measure the truncated text, and then use that measurement to more precisely adjust the height? Is there even a way to measure the height of the displayed text within the UITextView?
Edit
I need to clarify:
I do not need help truncating the text. As you can see from the screenshot, the text is truncated already.
I cannot measure the text because I would need to measure the the truncated text, which I don't have access to.
The gap at the bottom of the text view is not related to the textcontainerInset.
This is how the screenshot is currently built:
The text is set to some long string.
The text view is artificially constrained to some height, let's say 300. This produces the truncation.
Because 300 is not a precise multiple of the line height, there is some additional space below the last truncated line.
What I would like to do:
After sizing the text view to 300, measure the precise height of the truncated text so that I can then resize the text view a second time to fit it without the additional spacing (e.g. set it to 285 if that is the measured size).
Or, some other method to achieve the same end result.
I expect it's truncated because of your constraints. So you need to calculate the text size in the code and update the constraint value:
let boundingRect = (textView.text as NSString).boundingRect(
with: maxSize,
options: .usesLineFragmentOrigin,
attributes: [ .font: textView.font ],
context: nil
)
heightConstraint.constant = boundingRect.height.rounded(.up)
The result of this operation is float and not related to pixels. When the view size is calculated by the constraints, it get's rounded to pixels (0.5 or 0.333...), that's why we need to round it by ourself to exclude unpredictable cases. To get pixel perfect height you can round it according to screen scale:
textViewHeightConstraint.constant = (boundingRect.height * UIScreen.main.nativeScale).rounded(.up) / UIScreen.main.nativeScale
Also, as #nghiahoang mentioned below, don't forget to zero the insets
textView.textContainerInset = .zero
Here's my sample project
Compile the solution of Philip and set the inset.bottom to 0
textView.textContainerInset.bottom = 0
Directly, it is not possible. If you want to do it, you have take UILabel behind the UITextView. Because UILabel has the property to auto extend the height and width based on text. Now. set equal constraints of UILabel to UItextview.
what should be constraints, let me explore step by step :
set leading and trailing of UILabel.
Fix top constraints of uILabel.
you not need to set height constraint but if you want, you can set greater than equal to constraint of constant 20 or 30.
Now set leading, trailing,top and height constraints of UITextview equal to UILabel.
Now whatever you set text in UIlabel , give it to label as well. I did not in past. If you will find any issue please ask in comment

Set numeric UILabel for Auto Layout to correctly compute the intrinsic content size

I have a UILabel with a font of size 50 and text 1. At runtime, its text is changed to other numbers.
If I, say, center it in its superview, the automatically detected (intrinsic content size) height is a lot bigger than the actual text, and this is because it tries not to crop other lower characters like g.
The thing is that I know I won't use other characters than digits. I also don't want to set a fixed height constraint for it.
UIFont metrics include ascender, descender, cap height, x height, etc... all of which determines how the characters fit into a container. There is a good explanation and diagram here: http://cocoanetics.com/2010/02/understanding-uifont
If you really want to get the height (and/or width) of the individual character "glyphs" you'll need to use Core Text. This will include calling CTFontGetGlyphsForCharacters() and CTFontCreatePathForGlyph() to get the "glyph path" (a CGPath object), at which point you can get the "bounding box" to determine the exact size.
Lots of discussions and example code out there... A good starting point is simply searching for CTFontCreatePathForGlyph

How to vertically center text in UILabel with programmatic constraints after height constraint addition

I have a UILabel that I'm using inside a container view. I'm using AutoLayout entirely programmatically--no xib or storyboard. My UILabel constraints (no height constraint set, large fixed width, top and left edges pinned, numberOfLines = 0, bottom of superview pinned to bottom of label) work perfectly for normal (around 13 or 14 point) sized text, but at large (60 point) size text I started noticing large gaps of extra space above and below my character; in this case, the capital letter "S" that's an NSMutableAttributedString. The only attributes for this attributed string are the font name and a kerning value of 1.0. Below picture shows a magnified screenshot of my label from Pixie (white square shown is from that program) with my UILabel's backgroundColor in red:
After doing lots of research (Ignore Ascender and Descender when centering UILabel vertically?, iOS - Get the "real" height of a letter, Vertically center text in UILabel depending on actual visible letter height), it seemed the reason I was encountering this issue was because of the ascender and descender values for this font aren't getting used in this case (this is a capital "S," so it would seem I just need its capHeight value). So, I decided to constrict my label height to this value, and did so by setting a height constraint equal to its capHeight value after the "S" attributed text is set. Doing this produced the following image:
As you can see, the text is now clipped in the label. As a sort of hack, I figured I'd add some padding to counteract this, so multiplying my capHeight value by 1.1 yielded the following image:
That's fine for some padding, but now there's 4 px of extra space at the top. Granted I'm zoomed in very far, but I'm not sure why I'm seeing this—I would think that since the height constraint is set to the height of this capHeight value (which has some extra added for padding now) that the "S" would be vertically centered. I've tried calling setNeedsLayout on the label, but that didn't do anything. These are the things I would love to know:
Why is the capHeight value either not returning the correct value and/or setting the label's height constraint to this value is causing the text to get clipped?
Why do I need to add padding to the capHeight value?
Does anyone have any suggestions on how I can get this text vertically centered after I add padding and/or why it's not happening automatically? I saw this answer (https://stackoverflow.com/a/17376146/482557) but wasn't sure if that would apply here.
Thanks!

Fonts not fitting properly in UILabel - Ascender or Descender cropped

I've done extensive searching/reading/testing and cannot find a solution to this problem.
I've tried since iOS 4.3 and it's still not resolved in iOS7.
The problem is this: Fonts at large sizes can have their Ascenders or Descenders cropped in a UILabel.
Here's a screenshot directly from the Xcode 5.1 UI (no code at all!) showing the problem - font size 300 points:
As you can see, even a simple font like Helvetica Neue (bold or not) has it's Descender cropped. (You're seeing UIViewController > UIView > UILabel)
If you try this and then change the point size you'll see the font scale down, and eventually the Descender will not be cropped. Here it is again at 160 points:
Notice also that some fonts do not get cropped and others do - try Noteworthy, or Papyrus, or Savoye LET - all of which are standard iOS & fonts....
I'm talking about Height here - I know I can use adjustsFontSizeToFitWidth=YES to see the entire length, and I also know I can use sizeToFit, however neither guarantees no cropping of the Ascender/Descender.
Notice also that calculating the height using Ascender/Descender values does not help as the main issue is that the font is not centered vertically within the label when it is drawn. (If it were, it would be a simple calculation.)
So here is the question: How can I show a font as tall as possible and be assured that the Ascender/Descender is not cropped regardless of the font used?
EDIT:
I re-read my question and realized I did not ask it properly - I'm able to resize the label to fit the font - that's not the problem. Here's the revised question:
How can I draw text in a UILabel as large as possible and be assured that it is centered vertically, with no cropping of the Ascender or Descender?
I can easily figure out the overall height of the text, and once I know it will fit, how can draw it in the UILabel vertically centered?
For Example: In the first screenshot, the text "Tg" is cropped, but it is easily short enough to fit vertically in the label. In fact, it could be even larger and still fit if it were properly centered. But I know of no way to center it vertically...
The size of the label can be sized according the length of the string, the font attribute used and the size of the font. I use this method a lot and works great for such requirements -
NSString *textWithinLabel = #"Whatever you like, passed from where ever";
CGSize maximumLabelSize = CGSizeMake(300, 1000); //Place your maximum sizes here
//Here I've used Helvetica, though you can pass any font name or font size here to try out
NSDictionary *stringAttributes = [NSDictionary dictionaryWithObject:[[UIFont fontWithName:#"Helvetica" size:15] forKey: NSFontAttributeName];
CGSize newExpectedLabelSize = [textWithinLabel boundingRectWithSize:maximumLabelSize options:NSStringDrawingTruncatesLastVisibleLine|NSStringDrawingUsesLineFragmentOrigin attributes:stringAttributes context:nil].size;
CGRect frame = self.yourLabel.frame;
frame.size.height = newExpectedLabelSize.height;
self.yourLabel.frame = frame;
This example will change the height of the label, though you can use it to change width too etc.
The stringAttributes here are used to calculate the size, not to set the attributes. So for example, if your label is using 14pts and you calculate the height for 30pts, it won't change the height of the font, it will only increase the size of the label to accommodate the larger font size. If you want this method to also change the font attributes, you would need to add the appropriate code at the bottom of the method - self.yourLabel.text.font = ... etc.
I hope this answers your question,
Thanks, Jim.
I tried this and it solved my problem. Essentially, the height of the letter is Ascent+Descent. So that's all the space the label needs vertically.
1. [commentLabel sizeToFit]; //To trim out the unwanted area from the label
2. [commentLabel setFrame:CGRectMake(commentLabel.frame.origin.x, commentLabel.frame.origin.y + ABS(commentLabel.font.descender), commentLabel.frame.size.width, commentLabel.font.ascender + ABS(commentLabel.font.descender))];
//The frame adjustment in **(2)**moves the label down by commentLabel.font.descender because the label by default is aligned based on their actual bottom line instead of the actual line we use on notebooks, where the descender hangs down from the line. In case of a label the bottom line is the lower tip of the descender.

Determine UILabel height for centering when using adjustFontSizeToFitWidth

I have the following cell design where the numeric label shrinks and the "Overall" label is directly underneath.
I have properly set the adjustFontSizeToFitWidth and minimumFontSize properties. The font is resizing correctly. However, anchoring the numeric label to the bottom is challenging. Particularly when the font shrinks the gap between the two labels widens and does not appear vertically centered.
I have tried sizeToFit, sizeThatFits, and using the font's pointSize. All unsuccessfully.
I am aware of sizeWithFont:minFontSize:actualFontSize:forWidth:lineBreakMode:, but don't understand why I would need it in combination with adjustFontSizeToFitWidth.
Ah, so you want to position the UILabels in the middle of the container view (both horizontally and vertically)?
I have rephrased my answer so it will make more sense to future readers.
My code is assuming that you have the 3 IBOutlets set up:
UIView *containerView; //Your nice view containing the two textfields.
UILabel *points; //The label containing the number.
UILabel *overall; //The textfield containing the text 'overall'.
You could simply set the frame of the labels after assigning the text and calling the sizeToFit.
This first line positions the UILabel points, the only change being that the y coordinate is half of containerView subtract half of the height of itself.
points.frame = CGRectMake(points.frame.origin.x, (containerView.frame.size.height / 2) - (points.frame.size.height / 2), points.frame.size.width, points.frame.size.height);
To position the overall accordingly - say there is a distance of say 6 between the number and overall labels:
int space = 6;
overall.frame = CGRectMake(overall.frame.origin.x, points.frame.origin.y + points.frame.size.height + space, overall.frame.size.width, overall.frame.size.height);
Having read your comments, I think you are after this solution. If you want both UILabels to appear in the middle; subtract (overall.frame.size.height / 2) + (space / 2) from the y value of points like so (with the code of number 2 beneath it):
int space = 6;
points.frame = CGRectMake(points.frame.origin.x, ((containerView.frame.size.height / 2) - (points.frame.size.height / 2)) - ((overall.frame.size.height / 2) + (space / 2)), points.frame.size.width, points.frame.size.height);
overall.frame = CGRectMake(overall.frame.origin.x, points.frame.origin.y + points.frame.size.height + space, overall.frame.size.width, overall.frame.size.height);
The final point will produce an output like this image. As you can see the blue line is half of the whole image, and intersects the black rectangle (which is snuggly around the two labels) at its middle point. I hope this is what you were after.
Instead of using two labels, use CATextLayer instead. You will be easily able to make one part BOLD and the other normal. plus position and adjusting size for One layer will be easy relative to placing two labels. shadow setting, line break mode, fonts you will be able to adjust everything beautifully :)

Resources