Resizing a UILabel to accommodate insets
Asked Answered
O

8

68

I'm building a screen to scan barcodes, and I need to put a translucent screen behind some UILabels to improve visibility against light backgrounds.

Here's what the screen looks like now:

enter image description here

I'm setting the background color on the UILabel to get the translucent boxes. I've also created a custom UILabel subclass to allow me to set some padding between the edge of the UILabel and the text using this approach.

As you can see in the screen above, the UILabel doesn't resize correctly to take the padding into account. The "padding" just shifts the text over without changing the width of the label, causing the text to truncate.

Both of these labels will contain text of arbitrary lengths, and I really need the UILabel to dynamically resize.

What UILabel method can I override to increase the width of the label and factor in the padding?

Ocotillo answered 16/1, 2014 at 16:22 Comment(9)
How is this loaded? if you're using a storyboard, did you load this label using it? and if you did, did you add any constraints to this label and set the size to a constant number?Outstrip
Good questions. This view is constructed entirely in code. I'm using constraints to lay out the screen, and the only constraints on the labels set the position on the screen, not the size of the label.Ocotillo
ok, well it is most likely just an issue with the text being too long. I'd say just make the label wider entirely, or even adapting the width of the label depending on how long the text is within itOutstrip
Adapting the width of the label depending on the text length is exactly what I'm after, but calculating that outside of the UILabel is silly. I'm hoping to learn what UILabel method I can override to recalculate the size of the label to incorporate the padding.Ocotillo
check this out: #13237853Outstrip
Thanks, but sizeToFit doesn't take into account the padding. I tried overriding sizeToFit and sizeThatFits to increase the width of the rect, but it seems to have no effect.Ocotillo
Try to override -textRectForBounds:limitedToNumberOfLines: tooHeap
Possible duplicate of UILabel text marginEmbroideress
Extension for UILabel - Best solutionFredrickafredrickson
J
214

Here's a label class that calculates sizes correctly. The posted code is in Swift 3, but you can also download Swift 2 or Objective-C versions.

How does it work?

By calculating the proper textRect all of the sizeToFit and auto layout stuff works as expected. The trick is to first subtract the insets, then calculate the original label bounds, and finally to add the insets again.

Code (Swift 5)

class NRLabel: UILabel {
    var textInsets = UIEdgeInsets.zero {
        didSet { invalidateIntrinsicContentSize() }
    }
    
    override func textRect(forBounds bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect {
        let insetRect = bounds.inset(by: textInsets)
        let textRect = super.textRect(forBounds: insetRect, limitedToNumberOfLines: numberOfLines)
        let invertedInsets = UIEdgeInsets(
            top: -textInsets.top,
            left: -textInsets.left,
            bottom: -textInsets.bottom,
            right: -textInsets.right
        )
        return textRect.inset(by: invertedInsets)
    }
    
    override func drawText(in rect: CGRect) {
        super.drawText(in: rect.inset(by: textInsets))
    }
}

Optional: Interface Builder support

If you want to setup text insets in storyboards you can use the following extension to enable Interface Builder support:

@IBDesignable
extension NRLabel {

    // currently UIEdgeInsets is no supported IBDesignable type,
    // so we have to fan it out here:
    @IBInspectable
    var leftTextInset: CGFloat {
        set { textInsets.left = newValue }
        get { return textInsets.left }
    }

    // Same for the right, top and bottom edges.
}

Now you can conveniently setup your insets in IB and then just press ⌘= to adjust the label's size to fit.

Disclaimer:

All code is in the public domain. Do as you please.

Jessy answered 21/1, 2014 at 19:40 Comment(12)
For clearance; instead of manually setting the offset of rect you can use CGRectInset instead. Like this: return CGRectInset(rect, -insets.left, insets.top); developer.apple.com/library/mac/documentation/graphicsimaging/…Hypoglossal
@Hypoglossal Using CGRectInset only works when the insets are symmetrical.Jessy
@NikolaiRuhe Do we need to override intrinsicContentSize() as well? I saw somewhere it was required for AL. Maybe your trick does not involve this method?Sokol
@Sokol No, UILabel's implementation of intrinsicContentSize uses textRectForBounds to calculate the correct return value. Same is true for sizeToFit et al.Jessy
I would suggest adding ` if text == nil { return super.textRectForBounds(bounds, limitedToNumberOfLines: numberOfLines) }` to "hide" the label when there is no text. (Debacle
do we have to call some function in main class? Copying this code to UILabel class does not seems to workRiobard
@galambalazs I'd prefer if you post your addition in a comment.Jessy
@NikolaiRuhe sorry, I'm just trying to point all these answers to one place because the information is scattered about this issue. Your solution is very elegant. But I'm wandering, do you have any reference on auto layout automatically calling textRectForBounds? This has been my experience as well but the documentation only states: "This method may be called by the system if there was a prior call to the *sizeToFit or *sizeThatFits: method."Embroideress
Worked perfectly man. Thank you very much this. What I was trying before is I'm trying to override the intrinsicContentSize but I don't know why the f it doesn't work. :|Adriannaadrianne
Hey this worked great for me until I wanted to use firstBaseline alignment - seems that the baselines get completely messed up if text has flown over a line due to the insets applied. (i.e. if it would have been 1 line but, the insets bumped it to 2 lines).Alixaliza
why we should call invalidateIntrinsicContentSize()? it's look like doesn't bad effect when i comment this function.Rancorous
Thank you very much! This works also when using the label as viewForHeaderInSection in UITableView!!! Works like a charm and the label is resized correctly displaying all the text correctly!Sclerosis
R
15

Here is a Swift version of a UILabel subclass (same as @Nikolai's answer) that creates an additional padding around the text of a UILabel:

class EdgeInsetLabel : UILabel {
    var edgeInsets:UIEdgeInsets = UIEdgeInsetsZero

    override func textRectForBounds(bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect {
        var rect = super.textRectForBounds(UIEdgeInsetsInsetRect(bounds, edgeInsets), limitedToNumberOfLines: numberOfLines)

        rect.origin.x -= edgeInsets.left
        rect.origin.y -= edgeInsets.top
        rect.size.width  += (edgeInsets.left + edgeInsets.right);
        rect.size.height += (edgeInsets.top + edgeInsets.bottom);

        return rect
    }

    override func drawTextInRect(rect: CGRect) {
        super.drawTextInRect(UIEdgeInsetsInsetRect(rect, edgeInsets))
    }
}
Raseda answered 12/9, 2014 at 13:2 Comment(4)
Could you clarify in your answer what "solves the issue" means? Anyway, using your code and did upvote answer.Aerosol
Thanks! I prefer your code to the other since it does not require the extension - it's a simple subclass. Hmmm - you might want to make the subclass "final" too!Aerosol
This doesn't override intrinsic size - so it will truncate text if you are using an automatic sizing flow layoutPetcock
overriding textRectForBounds fixed a truncation issue, thanks!Pean
A
4

Here is the C# version (usefull for Xamarin) based on Nikolai's code :

public class UIEdgeableLabel : UILabel
{
    public UIEdgeableLabel() : base() { }
    public UIEdgeableLabel(NSCoder coder) : base(coder) { }
    public UIEdgeableLabel(CGRect frame) : base(frame) { }
    protected UIEdgeableLabel(NSObjectFlag t) : base(t) { }

    private UIEdgeInsets _edgeInset = UIEdgeInsets.Zero;
    public UIEdgeInsets EdgeInsets
    {
        get { return _edgeInset; }
        set
        {
            _edgeInset = value;
            this.InvalidateIntrinsicContentSize();
        }
    }

    public override CGRect TextRectForBounds(CGRect bounds, nint numberOfLines)
    {
        var rect = base.TextRectForBounds(EdgeInsets.InsetRect(bounds), numberOfLines);
        return new CGRect(x: rect.X - EdgeInsets.Left,
                          y: rect.Y - EdgeInsets.Top,
                          width: rect.Width + EdgeInsets.Left + EdgeInsets.Right,
                          height: rect.Height + EdgeInsets.Top + EdgeInsets.Bottom);
    }

    public override void DrawText(CGRect rect)
    {
        base.DrawText(this.EdgeInsets.InsetRect(rect));
    }
}
Anthraquinone answered 5/5, 2017 at 12:27 Comment(0)
B
4

Swift 5 version of Nikolai Ruhe answer:

extension UIEdgeInsets {
   func apply(_ rect: CGRect) -> CGRect {
      return rect.inset(by: self)
   }
}

class EdgeInsetLabel: UILabel {
  var textInsets = UIEdgeInsets.zero {
      didSet { invalidateIntrinsicContentSize() }
  }

  override func textRect(forBounds bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect {
    let insetRect = bounds.inset(by: textInsets)
    let textRect = super.textRect(forBounds: insetRect, limitedToNumberOfLines: numberOfLines)
    let invertedInsets = UIEdgeInsets(top: -textInsets.top,
                                      left: -textInsets.left,
                                      bottom: -textInsets.bottom,
                                      right: -textInsets.right)
    return textRect.inset(by: invertedInsets)
  }

  override func drawText(in rect: CGRect) {
      super.drawText(in: rect.inset(by: textInsets))
  }}
Bobby answered 8/4, 2019 at 15:48 Comment(0)
V
2

In additions to Nikolai Ruhe's answer, you need to invalidate intrinsic content size for autolayout to properly recalculate the size changes. You would notice this issue if you change edgeInsets over the application lifecycle:

class NRLabel: UILabel {

    var edgeInsets = UIEdgeInsetsZero {
        didSet {
            self.invalidateIntrinsicContentSize()
        }
    }

    ...
}
Volkslied answered 6/11, 2015 at 19:2 Comment(0)
V
0

Here is an example of what I used for a simple 10 unit padding on the left and right of the label with rounded corners. Just set the label text to center it's self and make it's class IndentedLabel and the rest takes care of itself. To modify the padding just scale up or down rect.size.width += (x)

class IndentedLabel: UILabel {

    var edgeInsets:UIEdgeInsets = UIEdgeInsetsZero

    override func textRectForBounds(bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect {
        var rect = super.textRectForBounds(UIEdgeInsetsInsetRect(bounds, edgeInsets), limitedToNumberOfLines: numberOfLines)

        rect.size.width  += 20;

        return rect
    }

    override func drawTextInRect(rect: CGRect) {
        self.clipsToBounds = true
        self.layer.cornerRadius = 3
        super.drawTextInRect(UIEdgeInsetsInsetRect(rect, edgeInsets))
    }
}
Virgy answered 19/4, 2016 at 15:19 Comment(0)
L
0

Here's a quick, hacky way to do it that you can understand more quickly. It's not as robust as Nikolai's, but it gets the job done. I did this when I was trying to fit my text in my UILabel within a UITableViewCell:

  1. Set a width constraint for the UILabel
  2. Connect the constraint via IBOutlet onto your code, either VC (custom cell class if you're doing an expanding table view cell)
  3. Create a variable for the actual size of the text, then add the insets + the width size to the constraint and update the view:

let messageTextSize: CGSize = (messageText as NSString).sizeWithAttributes([ NSFontAttributeName: UIFont.systemFontOfSize(14.0)]) cell.widthConstraint.constant = messageTextSize.width + myInsetsOrWhatever

I haven't extensively tested it yet, you might have to play around with the exact CGFloat values that you add. I found that the right size isn't exactly width plus insets; it's a little larger than that. This makes sure that the width of the UILabel will always be at least the text size or larger.

Lumberjack answered 28/9, 2016 at 20:43 Comment(0)
K
0

Swift 5 . You can create a custom UILabel class.
I've added 22 paddings to the left side of the content. When UILabel asks for intrinsicContentSize return by adding padding size you have added, I've added 22 and returned customized size. That's it.

// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation. 
override func draw(_ rect: CGRect) {
        // Drawing code
        let insets = UIEdgeInsets(top: 0, left: 22, bottom: 0, right: 0)
        super.drawText(in: rect.inset(by: insets))
        self.layoutSubviews()
}

// This will return custom size with flexible content size. Mainly it can be used in Chat.
override var intrinsicContentSize: CGSize {
        var size = super.intrinsicContentSize
        size.width = 22 + size.width
        return size
}
Kline answered 30/12, 2019 at 6:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.