UILabel italic font clipping
Asked Answered
C

8

6

I'm trying to solve this problem as in the application I'm working on I have a lot of fonts, so size of label is being calculated dynamically when user changes font.

The problem that I have is that UILabel is being clipped at the end if font is Italic like on picture bellow:

UILabel clipping example

This is what I have tried so far:

  • calculating of width with the help of CoreText and CGSize CTFramesetterSuggestFrameSizeWithConstraints ( CTFramesetterRef framesetter, CFRange stringRange, CFDictionaryRef frameAttributes, CGSize constraints, CFRange *fitRange );
  • calculating of width with the help of NSAttributedString and - (CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options context:(NSStringDrawingContext *)context
  • calculating of width with the help of NSString and - (CGSize)sizeWithAttributes:(NSDictionary<NSString *,id> *)attrs
  • using temp UITextView and sizeThatFits´ andfitToSize`

As there is a lot of fonts in application I need to set width of label dynamically, so subclassing of UILabel and adding few more points on drawFrameInRect is not working.

Here is sample code on Github.

Any help/advice is appreciated.

Customary answered 29/11, 2015 at 17:25 Comment(4)
Have you explored this: [your label view].clipsToBounds = NO;Bismuthinite
You should definitely look into autolayout.Pore
I remember having this problem since iOS 5 with italics. Especially when using right align. I was fixing it by adding a space char to the end of the text. It's a stupid workaround but it works.Kenny
Can you add a constant number to the width calculated? Or you need to calculate the width exactly?Inartificial
D
0

Even I don't have the time to test it, I'm pretty sure that the problem is that the label size is calculated from the advances. The advance is the amount of movement from one character's base point to the next ones. Typically for an italic font the advance can be smaller than the bounds. Therefore adding the advances will cut the end of the layout.

I.e. Baskerville-Italic H:

(lldb) p bounds[0].size
(CGSize) $6 = (width=33.53515625, height=26.484375)
(lldb) p advances[0]
(CGSize) $7 = (width=30, height=0)

So I think that you have to add the difference between advance and bounding box of the last character, if the text is layout in an ideal way (no compression and so on).

To get both values, you have to call some CT functions:

// What you probably have
CTFontRef font = …;
CFStringRef string = …;
CFAAttributedStringRef attrString = CFAttributedStringCreate(kCFAllocatorDefault, string, attributes);

// Get the character and glyph
CFIndex count = CFStringGetLength(string); // The length of the string
UniChar character;
CFStringGetCharacters( string, CFMakeRange( count-1, 1), &character );
CGGlyph glyph;
CTStringGetGlyphsForCharacters( font, &character, &glyph,1  );

// Get the advance
CGSize advance;
CTFontGetAdvancesForGlyphs( font, 0, &glyph, &advance, 1);

// Get the bounds
CGRect bounds;
CTFontGetAdvancesForGlyphs( font, 0, &glyph, &bounds, 1);

Typed in Safari.

Addition: You have the same problem at the left side: The advance to the uppercase L is 0 (the first character), but the bounding box has a negative x. Therefore the left side is clipped. Add space there, too.. (What is easier by far.)

Danitadaniyal answered 7/12, 2015 at 11:34 Comment(0)
C
2
CGSize size = [label.text sizeWithAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:17.0f]}];
CGSize adjustedSize = CGSizeMake(ceilf(size.width), ceilf(size.height));

And update the label's width and height from adjustedSize.Hope it works

Crandell answered 10/12, 2015 at 4:4 Comment(0)
W
1

I downloaded your GitHub project, and it appears that UILabel and UITextField are definitely calculating the bounds of the text differently. So, I first tried assigning the resulting size from sizeToFit on UITextField to the size of the UILabel. However, this approach breaks for the Strato font.

After trying some other approaches that failed, I remembered that italicized fonts sit at more or less the same angle. The italicized version of the font will usually increase the width of the bounding box by the same proportion to the original width.

Update:

Therefore, you should be able to derive the width of an italicized string from its width and font size. I've tried this approach in your project, and it works for all the fonts that you listed. I used the following formula:

CGRect calculatedRect = [attributedString boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:(NSStringDrawingUsesLineFragmentOrigin) context:nil];

calculatedRect.size.width = calculatedRect.size.width + (label.font.pointSize * 0.2);

Since this depends on the added width due to italicization, italicizing bold fonts, such as Strata will have a greater impact on the width.

Obviously, if and when the text API is augmented to include reliable calculations for text width, you will be able to adopt it. However, the workaround I've presented may still be a better choice, if you want to have backward compatibility.

I hope that helps!

Wichman answered 6/12, 2015 at 20:1 Comment(2)
That can't work. The increase in width doesn't depend on the width of the entire string but only on the last letter.Kenny
@Sulthan, that's a good point--I overlooked that. So it should be a function of the font size, instead of the width. Thank you, I'll test that, and update my answer.Wichman
T
1

This does the trick for me.

func labelThatFits(_ title: String) -> UILabel {
    let titleLabel = UILabel()
    titleLabel.font = UIFont(name: fontName, size: 20)
    titleLabel.text = title
    titleLabel.textAlignment = .center
    titleLabel.widthAnchor.constraint(equalToConstant: titleLabel.intrinsicContentSize.width + 10).isActive = true
    return titleLabel
}
Tybalt answered 4/8, 2018 at 21:10 Comment(0)
N
0

I would use Auto Layout on your buttons and on your label to always keep it centered based on the size of the font you choose your label to be.

Then, delete all of that code in the updateLabel method. I am not sure why you use UITextview and bring that jazz into the picture. If you click on your label in your storyboard and check out the attributes inspector and find "Autoshrink" it should be set to "Minimum Font Size" NOT "Fixed Font Size."

I would then use NSAttributedString on your label to change its size and font based on what button is clicked.

Nailhead answered 29/11, 2015 at 19:47 Comment(3)
UITextView is just for test, to see how it's calculation is working. If you try my example in debugging console you can see different type of widths based on different approaches and their calculations. That's the only reason why it's there. I'm not able to use auto layout on this project, but positioning is not a problem atm. Thanks for help.Customary
Was my answer in the direction you were heading to?Nailhead
A bit, but that's how I'm doing it right now without success. As I'm not able to use autolayout I have to calculate it manually and dynamically.Customary
D
0

Even I don't have the time to test it, I'm pretty sure that the problem is that the label size is calculated from the advances. The advance is the amount of movement from one character's base point to the next ones. Typically for an italic font the advance can be smaller than the bounds. Therefore adding the advances will cut the end of the layout.

I.e. Baskerville-Italic H:

(lldb) p bounds[0].size
(CGSize) $6 = (width=33.53515625, height=26.484375)
(lldb) p advances[0]
(CGSize) $7 = (width=30, height=0)

So I think that you have to add the difference between advance and bounding box of the last character, if the text is layout in an ideal way (no compression and so on).

To get both values, you have to call some CT functions:

// What you probably have
CTFontRef font = …;
CFStringRef string = …;
CFAAttributedStringRef attrString = CFAttributedStringCreate(kCFAllocatorDefault, string, attributes);

// Get the character and glyph
CFIndex count = CFStringGetLength(string); // The length of the string
UniChar character;
CFStringGetCharacters( string, CFMakeRange( count-1, 1), &character );
CGGlyph glyph;
CTStringGetGlyphsForCharacters( font, &character, &glyph,1  );

// Get the advance
CGSize advance;
CTFontGetAdvancesForGlyphs( font, 0, &glyph, &advance, 1);

// Get the bounds
CGRect bounds;
CTFontGetAdvancesForGlyphs( font, 0, &glyph, &bounds, 1);

Typed in Safari.

Addition: You have the same problem at the left side: The advance to the uppercase L is 0 (the first character), but the bounding box has a negative x. Therefore the left side is clipped. Add space there, too.. (What is easier by far.)

Danitadaniyal answered 7/12, 2015 at 11:34 Comment(0)
S
0

You will achieve much better results by using NSStringDrawingUsesDeviceMetrics in boundingRectWithSize:options:context:.

Sheliasheline answered 7/12, 2015 at 15:50 Comment(0)
P
0

1: Set the text to your label with your current selected font.

2: Use This method

- (CGSize)getSizeforController:(id)view 
{
    UILabel *tempView =(UILabel *)view;
    NSStringDrawingContext *context = [[NSStringDrawingContext alloc] init];
    context.minimumScaleFactor = 0.8;
    float height = tempView.frame.size.height;
    CGSize size=[tempView.text boundingRectWithSize:CGSizeMake(2000, height)options:NSStringDrawingUsesLineFragmentOrigin attributes:@{ NSFontAttributeName : tempView.font}context:context].size;
    return size;
}

3: Then set

CGSize newSize = [self.getSizeforController:self.lblYourLabel];
self.lblYourLabel.frame.size = newSize;

Hope this will work for you.

Proscenium answered 10/12, 2015 at 6:22 Comment(0)
P
0

Try this

CGSize lableWidth = CGSizeMake(300, CGFLOAT_MAX);
CGSize requiredSize = [yourLabel sizeWithFont:[UIFont fontWithName:@"FontName" size:17] constrainedToSize:lableWidth lineBreakMode:NSLineBreakByWordWrapping];
int calculatedHeight = requiredSize.height;
Pipsqueak answered 10/12, 2015 at 6:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.