UILabel clipping italic (oblique) text at left and right edges of content ( iOS 6+)
Asked Answered
T

4

20

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).

enter image description here

Here's a less subtle example using Zapfino, size 22. Note the 'j' in jupiter looks almost like an 'i':

enter image description here

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.

Tref answered 8/1, 2014 at 0:55 Comment(0)
T
24

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).

enter image description here

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.

enter image description here

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:

enter image description here

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.

enter image description here

Tref answered 8/1, 2014 at 0:55 Comment(5)
this is great, but it breaks line wrappingPathological
@Pathological Thanks for catching the word-wrapping issue. See my update for a fix.Tref
Super answer! If you like me have a problem with vertical clipping with characters like ÅÄÖ then swap the width for height and x for y in the codeMolality
important note. you should modify method sizeThatFits in case you want sizeToFit work properly, otherwise the content will be truncated after sending sizeToFit. Here is the code: -(CGSize)sizeThatFits:(CGSize)size { CGSize size_ = [super sizeThatFits:size]; size_.width += 2 * GUTTER; return size_; }Peridot
fantastic answer, implemented with @purrrminator 's method as I use sizeToFit, works perfectly, thanks both!!Escobar
I
4

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 {
        if #available(iOS 17, *) {
            // From iOS 17 onwards, it seems that when alignmentRectInsets changes, intrinsicContentSize automatically changes as well, so don't increase the intrinsicContentSize for iOS 17 and later.
            return super.intrinsicContentSize
        }
        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)
    }
    
}
Instructions answered 21/4, 2018 at 19:40 Comment(0)
H
1

Rather than having to go thru a bunch of gymnastics to work around this silly Apple bug (which I did), a quick-and-dirty hack is just add a space to the end of your string to stop the last italic letter being clipped. Obviously doesn't help with multi-line labels alas, or clipped first letter descender...

Hydromel answered 10/7, 2018 at 1:0 Comment(1)
This was enough for me :-)Antrorse
H
1

Swift and SwiftUI version based on Vadim Akhmerov and bilobatum's answers. All four edges are now customizable and can be changed/updated.

Also hosted as a Github Gist: https://gist.github.com/ryanlintott/2340f35977bf2d1f7b6ea40aa379bcc6

import SwiftUI
import UIKit

struct NoClipText: UIViewRepresentable {
    typealias UIViewType = NoClipLabel
    
    let text: String
    let font: UIFont
    let clipExtension: EdgeSizes

    func makeUIView(context: Context) -> UIViewType {
        let uiView = UIViewType()
        uiView.text = text
        uiView.font = font
        uiView.clipExtension = clipExtension
        return uiView
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) {
        uiView.text = text
        uiView.font = font
        uiView.clipExtension = clipExtension
    }
}

class NoClipLabel: UILabel {
    static let defaultClipExtension: EdgeSizes = .all(10)
    
    var clipExtension: EdgeSizes
    
    var top: CGFloat { clipExtension.top }
    var left: CGFloat { clipExtension.left }
    var bottom: CGFloat { clipExtension.bottom }
    var right: CGFloat { clipExtension.right }
    var width: CGFloat { left + right }
    var height: CGFloat { bottom + top }
    
    required init(clipExtension: EdgeSizes = defaultClipExtension) {
        self.clipExtension = clipExtension
        super.init(frame: CGRect.zero)
    }

    override init(frame: CGRect) {
        clipExtension = Self.defaultClipExtension
        super.init(frame: frame)
    }
    
    required init?(coder aDecoder: NSCoder) {
        clipExtension = Self.defaultClipExtension
        super.init(coder: aDecoder)
    }
    
    override func draw(_ rect: CGRect) {
        super.drawText(in: rect.inset(by: UIEdgeInsets(top: top, left: left, bottom: bottom, right: right)))
    }

    override var alignmentRectInsets: UIEdgeInsets {
        return .init(top: top, left: left, bottom: bottom, right: right)
    }

    override var intrinsicContentSize: CGSize {
        var size = super.intrinsicContentSize
        size.width += width
        size.height += height

        return size
    }

    override func sizeThatFits(_ size: CGSize) -> CGSize {
        let fixedSize = CGSize(width: size.width - width, height: size.height - height)
        let sizeWithoutExtension = super.sizeThatFits(fixedSize)

        return CGSize(width: sizeWithoutExtension.width + width,
                      height: sizeWithoutExtension.height + height)
    }

}

struct EdgeSizes: Equatable {
    let top: CGFloat
    let left: CGFloat
    let bottom: CGFloat
    let right: CGFloat
    
    init(top: CGFloat = 0, left: CGFloat = 0, bottom: CGFloat = 0, right: CGFloat = 0) {
        self.top = top
        self.left = left
        self.bottom = bottom
        self.right = right
    }
    
    init(vertical: CGFloat = 0, horizontal: CGFloat = 0) {
        self.top = vertical
        self.left = horizontal
        self.bottom = vertical
        self.right = horizontal
    }
    
    init(_ all: CGFloat) {
        self.top = all
        self.left = all
        self.bottom = all
        self.right = all
    }
    
    static let zero = EdgeSizes(0)
    
    static func all(_ size: CGFloat) -> EdgeSizes {
        EdgeSizes(size)
    }
    
    static func vertical(_ size: CGFloat) -> EdgeSizes {
        EdgeSizes(vertical: size)
    }
    
    static func horizontal(_ size: CGFloat) -> EdgeSizes {
        EdgeSizes(horizontal: size)
    }
}
Hachman answered 28/10, 2020 at 1:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.