Problem:
UILabel
may clip italic (oblique) characters and even scripts at the left and right edges. The following screenshot displays the issue. At the left edge, the descender of the 'j' is clipped; at the right edge, the ascender of the 'l' is clipped. I realize this is subtle, and not everyone is going to care (however, the issue gets worse with larger font sizes).
Here's a less subtle example using Zapfino, size 22. Note the 'j' in jupiter looks almost like an 'i':
In the examples above, the background color of the label is orange, the text is left aligned, and the label maintains its intrinsic content size.
This is the default behavior of a UILabel
and its been that way for multiple versions of iOS (so I'm not expecting a fix from Apple).
What I have tried:
Setting the label's clipsToBounds
property to NO
does not resolve the issue. I'm also aware that I could set a fixed width constraint on the label to give the text more room at the trailing edge. However, a fixed width constraint would not give the 'j', in the example above, more room.
I'm going to answer my own question using a solution that leverages Auto Layout and the label's alignmentRectInsets
.
The top label shows the default behavior of a UILabel
when the text is left aligned that the label maintains its intrinsic content size. The bottom label is a simple (almost trivial) subclass of UILabel
. The bottom label does not clip the 'j' or the 'l'; instead, it gives the text some room to breathe at the left and right edges without center aligning the text (yuck).
Although the labels themselves don't appear aligned on screen, their text does appear aligned; and what's more, in IB, the labels actually have their left edges aligned because I override alignmentRectInsets
in a UILabel
subclass.
Here's the code that configures the two labels:
#import "ViewController.h"
#import "NonClippingLabel.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UILabel *topLabel;
@property (weak, nonatomic) IBOutlet NonClippingLabel *bottomLabel;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
NSString *string = @"jupiter ariel";
UIFont *font = [UIFont fontWithName:@"Helvetica-BoldOblique" size:28];
NSDictionary *attributes = @{NSFontAttributeName: font};
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string attributes:attributes];
self.topLabel.attributedText = attrString;
self.bottomLabel.attributedText = attrString;
}
Here's the implementation of the NonClippingLabel
subclass:
#import <UIKit/UIKit.h>
@interface NonClippingLabel : UILabel
@end
@implementation NonClippingLabel
#define GUTTER 4.0f // make this large enough to accommodate the largest font in your app
- (void)drawRect:(CGRect)rect
{
// fixes word wrapping issue
CGRect newRect = rect;
newRect.origin.x = rect.origin.x + GUTTER;
newRect.size.width = rect.size.width - 2 * GUTTER;
[self.attributedText drawInRect:newRect];
}
- (UIEdgeInsets)alignmentRectInsets
{
return UIEdgeInsetsMake(0, GUTTER, 0, GUTTER);
}
- (CGSize)intrinsicContentSize
{
CGSize size = [super intrinsicContentSize];
size.width += 2 * GUTTER;
return size;
}
@end
No editing a font file, no using Core Text; just a relatively simple UILabel
subclass for those using iOS 6+ and Auto Layout.
Update:
Augie caught the fact that my original solution prevented word wrapping for multi-lined text. I fixed that issue by using drawInRect:
instead of drawAtPoint:
to draw the text in the label's drawRect:
method.
Here's a screenshot:
The top label is a plain-vanilla UILabel
. The bottom label is a NonClippingLabel
with an extreme gutter setting to accommodate Zapfino at size 22.0. Both labels are left and right aligned using Auto Layout.
Swift version of NonClippingLabel
with fixed sizeThatFits
method from bilobatum answer.
class NonClippingLabel: UILabel {
let gutter: CGFloat = 4
override func draw(_ rect: CGRect) {
super.drawText(in: rect.insetBy(dx: gutter, dy: 0))
}
override var alignmentRectInsets: UIEdgeInsets {
return .init(top: 0, left: gutter, bottom: 0, right: gutter)
}
override var intrinsicContentSize: CGSize {
var size = super.intrinsicContentSize
size.width += gutter * 2
return size
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
let fixedSize = CGSize(width: size.width - 2 * gutter, height: size.height)
let sizeWithoutGutter = super.sizeThatFits(fixedSize)
return CGSize(width: sizeWithoutGutter.width + 2 * gutter,
height: sizeWithoutGutter.height)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With