Resizing a UILabel to accommodate insets

IosIphone

Ios Problem Overview


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?

Ios Solutions


Solution 1 - Ios

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.

Solution 2 - Ios

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

Solution 3 - Ios

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));
	}
}

Solution 4 - Ios

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

Solution 5 - Ios

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()
        }
    }

    ...
}

Solution 6 - Ios

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

Solution 7 - Ios

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.

Solution 8 - Ios

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
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJosh EarlView Question on Stackoverflow
Solution 1 - IosNikolai RuheView Answer on Stackoverflow
Solution 2 - IosKlaasView Answer on Stackoverflow
Solution 3 - IosPataphysicienView Answer on Stackoverflow
Solution 4 - IosKaiser AblizView Answer on Stackoverflow
Solution 5 - IosMax S.View Answer on Stackoverflow
Solution 6 - IosblackopsView Answer on Stackoverflow
Solution 7 - IosHuy-Anh HoangView Answer on Stackoverflow
Solution 8 - IosKiran JasvaneeView Answer on Stackoverflow