mag_zbc
mag_zbc

Reputation: 6982

UILabel subclass - text cut off in bottom despite label being correct height

I have a problem with UILabel subclass cutting off text in the bottom. Label is of proper height to fit the text, there is some space left in the bottom, but the text is still being cut off.

The label

The red stripes are border added to label's layer.

I subclass the label to add edge insets.

override func sizeThatFits(size: CGSize) -> CGSize {
    var size = super.sizeThatFits(size)
    size.width += insets.left + insets.right
    size.height += insets.top + insets.bottom
    return size
}

override func drawTextInRect(rect: CGRect) {
    super.drawTextInRect(UIEdgeInsetsInsetRect(rect, insets))
}

However, in this particular case the insets are zero.

Upvotes: 33

Views: 32290

Answers (8)

flopshot
flopshot

Reputation: 1645

I had a vertical UIStackView with a UILabel at the bottom. This UILabel was cutting off the letters that go below the baseline (q, g, y, etc), but only when nested inside a horizontal UIStackView. The fix was to add the .lastBaseline alignment modifier to the outer stack view.

    lazy var stackView: UIStackView = {
        let stackView = UIStackView(arrangedSubviews: [
            aVerticalStackWithUILabelAtBottom, // <-- bottom UILabel was cutoff
            UIView(),
            someOtherView 
        ])
        stackView.axis = .horizontal
        stackView.spacing = Spacing.one
        stackView.alignment = .lastBaseline // <-- BOOM fixed it
        stackView.isUserInteractionEnabled = true
        return stackView
    }()

Upvotes: 2

luizv
luizv

Reputation: 638

TL'DR

Probably the property you are looking for is UILabel's baselineAdjustment.

It is needed because of an old UILabel's known bug. Try it:

label.baselineAdjustment = .none

Also it could be changed through interface builder. This property could be found under UILabel's Attributes inspector with the name "Baseline".

Baseline property on Interface Builder


Explanation

It's a bug

There is some discussions like this one about a bug on UILabel's text bounding box. What we observe here in our case is some version of this bug. It looks like the bounding box grows in height when we shrink the text through AutoShrink .minimumFontScale or .minimumFontSize.

As a consequence, the bounding box grows bigger than the line height and the visible portion of UILabel's height. That said, with baselineAdjustment property set to it's default state, .alignBaselines, text aligns to the cropped bottom and we could observe line clipping.

Understanding this behaviour is crucial to explain why set .alignCenters solve some problems but not others. Just center text on the bigger bounding box could still clip it.


Solution

So the best approach is to set

label.baselineAdjustment = .none

The documentation for the .none case said:

Adjust text relative to the top-left corner of the bounding box. This is the default adjustment.

Since bonding box origin matches the label's frame, it should fix any problem for a one-lined label with AutoShrink enabled.

Also it could be changed through interface builder. This property could be found under UILabel's Attributes inspector with the name "Baseline".


Documentation

You could read more here about UILabel's baselineAdjustmenton official documentation.

Upvotes: 8

Mohit Singh
Mohit Singh

Reputation: 1450

I was facing the same issue with Helvetica Neue Condensed Bold font. Changing label's Baseline property from Align Baselines to Align Centers did the trick for me. You can change this easily in storyboard by selecting your label.

Upvotes: 12

Robin Douglas
Robin Douglas

Reputation: 43

I ran into this too, but wanted to avoid adding a height constraint. I'd already created a UILabel subclass that allowed me to add content insets (but for the purpose of setting tableHeaderView straight to a label without having to contain it in another view). Using the class I could set the bottom inset to solve the issue with the font clipping.

import UIKit

@IBDesignable class InsetLabel: UILabel {

    @IBInspectable var topInset: CGFloat = 16
    @IBInspectable var bottomInset: CGFloat = 16
    @IBInspectable var leftInset: CGFloat = 16
    @IBInspectable var rightInset: CGFloat = 16
    var insets: UIEdgeInsets {
        get {
            return UIEdgeInsets(
                top: topInset,
                left: leftInset,
                bottom: bottomInset,
                right: rightInset
            )
        }
    }

    override func drawText(in rect: CGRect) {
        super.drawText(in: rect.inset(by: insets))
    }

    override var intrinsicContentSize: CGSize {
        return addInsetsTo(size: super.intrinsicContentSize)
    }

    override func sizeThatFits(_ size: CGSize) -> CGSize {
        return addInsetsTo(size: super.sizeThatFits(size))
    }

    func addInsetsTo(size: CGSize) -> CGSize {
        return CGSize(
            width: size.width + leftInset + rightInset,
            height: size.height + topInset + bottomInset
        )
    }

}

This could be simplified just for the font clipping to:

import UIKit

class FontFittingLabel: UILabel {

    var inset: CGFloat = 16 // Adjust this

    override func drawText(in rect: CGRect) {
        super.drawText(in: rect.inset(by: UIEdgeInsets(
            top: 0,
            left: 0,
            bottom: inset,
            right: 0
        )))
    }

    override var intrinsicContentSize: CGSize {
        let size = super.intrinsicContentSize
        return CGSize(
            width: size.width,
            height: size.height + inset
        )
    }

}

Upvotes: 3

juliand665
juliand665

Reputation: 3324

My problem was that the label's (vertical) content compression resistance priority was not high enough; setting it to required (1000) fixed it.

It looks like the other non-OP answers may be some sort of workaround for this same underlying issue.

Upvotes: 12

shoe
shoe

Reputation: 1070

Other answers didn't help me, but what did was constraining the height of the label to whatever height it needed, like so:

let unconstrainedSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
label.heightAnchor.constraint(equalToConstant: label.sizeThatFits(unconstrainedSize).height).isActive = true

Also, sizeThatFits(_:) will return a 0 by 0 size if your label's text field is nil or equal to ""

Upvotes: 2

AlekseiPetrovski
AlekseiPetrovski

Reputation: 1106

Happened for me when providing topAnchor and centerYAnchor for label at the same time. Leaving just one anchor fixed the problem.

Upvotes: 3

mag_zbc
mag_zbc

Reputation: 6982

Turns out the problem was with

self.lineBreakMode = .ByClipping

changing it to

self.lineBreakMode = .ByCharWrapping

Solved the problem

Upvotes: 22

Related Questions