iOS UILabel last lines cut off of long text with Subclassed insets - ios

I have been attempting to get insets working with a UILabel by using this subclassed method:
#IBDesignable class TextLabelWithInsets: UILabel {
#IBInspectable var topInset: CGFloat = 0.0
#IBInspectable var leftInset: CGFloat = 0.0
#IBInspectable var bottomInset: CGFloat = 0.0
#IBInspectable var rightInset: CGFloat = 0.0
var insets: UIEdgeInsets {
get {
return UIEdgeInsetsMake(topInset, leftInset, bottomInset, rightInset)
}
set {
topInset = newValue.top
leftInset = newValue.left
bottomInset = newValue.bottom
rightInset = newValue.right
}
}
override func drawText(in rect: CGRect) {
super.drawText(in: UIEdgeInsetsInsetRect(rect, insets))
}
override var intrinsicContentSize: CGSize {
var contentSize = super.intrinsicContentSize
contentSize.width += leftInset + rightInset
contentSize.height += topInset + bottomInset
print("Content Size: " + String(describing: contentSize))
return contentSize
}
}
The problem is that this only works for smaller blocks of text. It seems as if as soon as the text gets long enough, the last line is cut off.
Any ideas how this can be fixed? Adding extra pixels to the height works but makes smaller ones look dumb. I dont know what the problem is here, the code makes sense in my head.
Here is the rest in case this helps:
func commonInit() {
oppositeSideEdgeSpacing = screen.size.width/4
textLabel = TextLabelWithInsets(frame: .zero)
textLabel.layer.cornerRadius = bubbleCornerRadius
textLabel.clipsToBounds = true
textLabel.topInset = bubbleTopInsetSpacing
textLabel.bottomInset = bubbleBottomInsetSpacing
textLabel.leftInset = bubbleLeftInsetSpacing
textLabel.rightInset = bubbleRightInsetSpacing
textLabel.numberOfLines = 0
textLabel.translatesAutoresizingMaskIntoConstraints = false
textLabel.textAlignment = .left
textLabel.backgroundColor = UIColor(hexString: "CDDCE0") // TODO: Fix this
addSubview(textLabel)
if !isMessageFromCurrentUser {
textLabel.topAnchor.constraint(equalTo: topAnchor, constant: bubbleEdgeSpacing).set(active: true)
textLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: bubbleEdgeSpacing).set(active: true)
textLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -oppositeSideEdgeSpacing).set(active: true)
textLabel.bottomAnchor.constraint(equalTo: bottomAnchor).set(active: true)
}
}

Related

How to update a UILabel width dynamically without overloading the CPU

Labels in iOS are create like (1), no horizontal margin and no beauty at all.
I would like to create a label like in (2), curved edges and a margin left and right
The contents of this label is updated 2 times per second and its width must change dynamically.
So I have created this class
#IBDesignable
class BeautifulLabel : UILabel {
// private var internalRect : CGRect? = .zero
override func drawText(in rect: CGRect) {
let insets = UIEdgeInsets(top: marginTop,
left: marginLeft,
bottom: marginBottom,
right: marginRight)
super.drawText(in: rect.inset(by: insets))
}
#IBInspectable var cornerRadius: CGFloat = 0 {
didSet {
self.layer.cornerRadius = cornerRadius
self.layer.masksToBounds = cornerRadius > 0
}
}
#IBInspectable var marginTop: CGFloat = 0
#IBInspectable var marginBottom: CGFloat = 0
#IBInspectable var marginLeft: CGFloat = 0
#IBInspectable var marginRight: CGFloat = 0
override func layoutSubviews() {
super.layoutSubviews()
var bounds = self.bounds
bounds.size.width += marginLeft + marginRight
bounds.size.height += marginTop + marginBottom
self.bounds = bounds
}
This works but adjusting self.bounds inside layoutSubviews(), makes this method to be called again, resulting in a huge loop, CPU spike and memory leak.
Then I tried this:
override var text: String? {
didSet {
let resizingLabel = UILabel(frame: self.bounds)
resizingLabel.text = self.text
var bounds = resizingLabel.textRect(forBounds: CGRect(x: 0, y: 0, width: 500, height: 50), limitedToNumberOfLines: 1)
bounds.size.width += marginLeft + marginRight
bounds.size.height += marginTop + marginBottom
self.bounds = bounds
}
}
this simply does not work. Label is not adjusted to the proper size.
The label must have just one line, fixed height, truncated tail and fixed font size (System 17). I am interested in its width.
Any ideas?
A view should not change its own size. It should only change its intrinsicContentSize.
When you add a view to the view hierarchy, that’s when you specify whether it should observe the intrinsic content size or not (e.g. content hugging settings, compression resistance, absence of explicit width and height constraints, etc.). If you do this, the auto layout engine will do everything for you.
So, by way of example, a minimalist approach would be something that just overrides intrinsicContentSize:
#IBDesignable
class BeautifulLabel: UILabel {
#IBInspectable var marginX: CGFloat = 0 { didSet { invalidateIntrinsicContentSize() } }
#IBInspectable var marginY: CGFloat = 0 { didSet { invalidateIntrinsicContentSize() } }
#IBInspectable var cornerRadius: CGFloat = 0 { didSet { layer.cornerRadius = cornerRadius } }
override var intrinsicContentSize: CGSize {
let size = super.intrinsicContentSize
return CGSize(width: size.width + marginX * 2, height: size.height + marginY * 2)
}
}
A more complete example might be a UIView subclass, where the label is a subview, inset by the appropriate margins:
#IBDesignable
class BeautifulLabel: UIView {
#IBInspectable var marginTop: CGFloat = 0 { didSet { didUpdateInsets() } }
#IBInspectable var marginBottom: CGFloat = 0 { didSet { didUpdateInsets() } }
#IBInspectable var marginLeft: CGFloat = 0 { didSet { didUpdateInsets() } }
#IBInspectable var marginRight: CGFloat = 0 { didSet { didUpdateInsets() } }
#IBInspectable var cornerRadius: CGFloat = -1 { didSet { setNeedsLayout() } }
#IBInspectable var text: String? {
get {
label.text
}
set {
label.text = newValue
invalidateIntrinsicContentSize()
}
}
#IBInspectable var font: UIFont? {
get {
label.font
}
set {
label.font = newValue
invalidateIntrinsicContentSize()
}
}
private var topConstraint: NSLayoutConstraint!
private var leftConstraint: NSLayoutConstraint!
private var rightConstraint: NSLayoutConstraint!
private var bottomConstraint: NSLayoutConstraint!
private let label: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override var intrinsicContentSize: CGSize {
let size = label.intrinsicContentSize
return CGSize(width: size.width + marginLeft + marginRight,
height: size.height + marginTop + marginBottom)
}
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
override func layoutSubviews() {
super.layoutSubviews()
let maxCornerRadius = min(bounds.width, bounds.height) / 2
if cornerRadius < 0 || cornerRadius > maxCornerRadius {
layer.cornerRadius = maxCornerRadius
} else {
layer.cornerRadius = cornerRadius
}
}
}
private extension BeautifulLabel {
func configure() {
addSubview(label)
topConstraint = label.topAnchor.constraint(equalTo: topAnchor, constant: marginTop)
leftConstraint = label.leftAnchor.constraint(equalTo: leftAnchor, constant: marginLeft)
rightConstraint = rightAnchor.constraint(equalTo: label.rightAnchor, constant: marginRight)
bottomConstraint = bottomAnchor.constraint(equalTo: label.bottomAnchor, constant: marginBottom)
NSLayoutConstraint.activate([leftConstraint, rightConstraint, topConstraint, bottomConstraint])
}
func didUpdateInsets() {
topConstraint.constant = marginTop
leftConstraint.constant = marginLeft
rightConstraint.constant = marginRight
bottomConstraint.constant = marginBottom
invalidateIntrinsicContentSize()
}
}
Now in this case, I'm only exposing text and font, but you'd obviously repeat for whatever other properties you want to expose.
But let’s not get lost in the details of the above implementation. The bottom line is that a view should not attempt to adjust its own size, but rather merely its own intrinsicContentSize. And it should perform invalidateIntrinsicContentSize where necessary.

UIImage content mode aspectFit and bottom

Is it possible to set the contentMode for my UIImage to .scaleAspectFit and .bottom simultaneously ?
This is how my image looks like at the moment:
UIImageView:
let nightSky: UIImageView = {
let v = UIImageView()
v.image = UIImage(named: "nightSky")
v.translatesAutoresizingMaskIntoConstraints = false
v.contentMode = .scaleAspectFit
return v
}()
Constraints:
nightSky.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
nightSky.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -120).isActive = true
nightSky.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 30).isActive = true
nightSky.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -30).isActive = true
Here is a custom class that allows Aspect Fit and Alignment properties.
It is marked #IBDesignable so you can see it in Storyboard / Interface Builder.
The #IBInspectable properties are:
Image
Horizontal Alignment
Vertical Alignment
Aspect Fill
Select the image as you would for a normal UIImageView.
Valid values for HAlign are "left" "center" "right" or leave blank for default (center).
Valid values for VAlign are "top" "center" "bottom" or leave blank for default (center).
"Aspect Fill" is On or Off (True/False). If True, the image will be scaled to Aspect Fill instead of Aspect Fit.
#IBDesignable
class AlignedAspectFitImageView: UIView {
enum HorizontalAlignment: String {
case left, center, right
}
enum VerticalAlignment: String {
case top, center, bottom
}
private var theImageView: UIImageView = {
let v = UIImageView()
return v
}()
#IBInspectable var image: UIImage? {
get { return theImageView.image }
set {
theImageView.image = newValue
setNeedsLayout()
}
}
#IBInspectable var hAlign: String = "center" {
willSet {
// Ensure user enters a valid alignment name while making it lowercase.
if let newAlign = HorizontalAlignment(rawValue: newValue.lowercased()) {
horizontalAlignment = newAlign
}
}
}
#IBInspectable var vAlign: String = "center" {
willSet {
// Ensure user enters a valid alignment name while making it lowercase.
if let newAlign = VerticalAlignment(rawValue: newValue.lowercased()) {
verticalAlignment = newAlign
}
}
}
#IBInspectable var aspectFill: Bool = false {
didSet {
setNeedsLayout()
}
}
var horizontalAlignment: HorizontalAlignment = .center
var verticalAlignment: VerticalAlignment = .center
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
commonInit()
}
func commonInit() -> Void {
clipsToBounds = true
addSubview(theImageView)
}
override func layoutSubviews() {
super.layoutSubviews()
guard let img = theImageView.image else {
return
}
var newRect = bounds
let viewRatio = bounds.size.width / bounds.size.height
let imgRatio = img.size.width / img.size.height
// if view ratio is equal to image ratio, we can fill the frame
if viewRatio == imgRatio {
theImageView.frame = newRect
return
}
// otherwise, calculate the desired frame
var calcMode: Int = 1
if aspectFill {
calcMode = imgRatio > 1.0 ? 1 : 2
} else {
calcMode = imgRatio < 1.0 ? 1 : 2
}
if calcMode == 1 {
// image is taller than wide
let heightFactor = bounds.size.height / img.size.height
let w = img.size.width * heightFactor
newRect.size.width = w
switch horizontalAlignment {
case .center:
newRect.origin.x = (bounds.size.width - w) * 0.5
case .right:
newRect.origin.x = bounds.size.width - w
default: break // left align - no changes needed
}
} else {
// image is wider than tall
let widthFactor = bounds.size.width / img.size.width
let h = img.size.height * widthFactor
newRect.size.height = h
switch verticalAlignment {
case .center:
newRect.origin.y = (bounds.size.height - h) * 0.5
case .bottom:
newRect.origin.y = bounds.size.height - h
default: break // top align - no changes needed
}
}
theImageView.frame = newRect
}
}
Using this image:
Here's how it looks with a 240 x 240 AlignedAspectFitImageView with background color set to yellow (so we can see the frame):
Properties can also be set via code. For example:
override func viewDidLoad() {
super.viewDidLoad()
let testImageView = AlignedAspectFitImageView()
testImageView.image = UIImage(named: "bkg640x360")
testImageView.verticalAlignment = .bottom
view.addSubview(testImageView)
// set frame / constraints / etc
testImageView.frame = CGRect(x: 40, y: 40, width: 240, height: 240)
}
To show the difference between "Aspect Fill" and "Aspect Fit"...
Using this image:
We get this result with Aspect Fill: Off and VAlign: bottom:
and then this result with Aspect Fill: On and HAlign: right:
Set the UIImageView's top layout constraint priority to lowest (i.e. 250) and it will handle it for you.

Setting padding to UILabel crops characters

I'm adding padding to UILabel
public partial class MessageLabel : UILabel
{
public MessageLabel(IntPtr handle) : base(handle)
{
}
public override void DrawText(CoreGraphics.CGRect rect)
{
UIEdgeInsets insets = new UIEdgeInsets() { Bottom = 5, Top = 5, Left = 5, Right = 5 };
base.DrawText(insets.InsetRect(rect));
}
}
Padding are added well, but last line is cropped, due to extra empty space I added... If I increate height with 10 in this methods, padding are added only at the top of label.
with paddings screen 1 - looks nice, but missing words:
without passings screen 2 - show whole message:
Any solution?
Try this:
private var topInset: CGFloat = 5
private var bottomInset: CGFloat = 5
private var leftInset: CGFloat = 5
private var rightInset: CGFloat = 5
override func drawTextInRect(rect: CGRect) {
let insets = UIEdgeInsets(top: topInset, left: leftInset, bottom: bottomInset, right: rightInset)
super.drawTextInRect(UIEdgeInsetsInsetRect(rect, insets))
}
override public var intrinsicContentSize: CGSize {
var intrinsicSuperViewContentSize = super.intrinsicContentSize
intrinsicSuperViewContentSize.height += topInset + bottomInset
intrinsicSuperViewContentSize.width += leftInset + rightInset
return intrinsicSuperViewContentSize
}

How to set padding for UILabel using #IBDesignable property?

This is how looks my subclass for UILabel:
#IBDesignable class AttributedLabel: UILabel {
#IBInspectable var padding: CGFloat = 0
override func drawTextInRect(rect: CGRect) {
super.drawTextInRect(UIEdgeInsetsInsetRect(rect, UIEdgeInsetsMake(padding, padding, padding, padding)))
}
}
I correctly set padding in storyboard but It doesnt work because padding is still 0.
What to do to make it working? Is it possible to render it live in Storyboard?
Use like this; Change top , bottom, left, right inset paddings.
#IBDesignable class AttributedLabel: UILabel {
#IBInspectable var topInset: CGFloat = 5.0
#IBInspectable var bottomInset: CGFloat = 5.0
#IBInspectable var leftInset: CGFloat = 7.0
#IBInspectable var rightInset: CGFloat = 7.0
override func drawTextInRect(rect: CGRect) {
let insets = UIEdgeInsets(top: topInset, left: leftInset, bottom: bottomInset, right: rightInset)
super.drawTextInRect(UIEdgeInsetsInsetRect(rect, insets))
}
override func intrinsicContentSize() -> CGSize {
var intrinsicSuperViewContentSize = super.intrinsicContentSize()
intrinsicSuperViewContentSize.height += topInset + bottomInset
intrinsicSuperViewContentSize.width += leftInset + rightInset
return intrinsicSuperViewContentSize
}
}
Thanks
Your subclass looks incomplete. As mentioned in the documentation, you should override both of these methods :
public func textRectForBounds(bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect
public func drawTextInRect(rect: CGRect)
Here is an example implementation that should work :
#IBDesignable class AttributedLabel : UILabel
{
#IBInspectable var padding: CGFloat = 0 {
didSet {
self.textInsets = UIEdgeInsets(top: self.padding, left: self.padding, bottom: self.padding, right: self.padding)
}
}
var textInsets = UIEdgeInsetsZero {
didSet {
self.invalidateIntrinsicContentSize()
}
}
override func textRectForBounds(bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect
{
var insets = self.textInsets
let insetRect = UIEdgeInsetsInsetRect(bounds, insets)
let textRect = super.textRectForBounds(insetRect, limitedToNumberOfLines: numberOfLines)
insets = UIEdgeInsets(top: -insets.top, left: -insets.left, bottom: -insets.bottom, right: -insets.right)
return UIEdgeInsetsInsetRect(textRect, insets)
}
override func drawTextInRect(rect: CGRect) {
super.drawTextInRect(UIEdgeInsetsInsetRect(rect, self.textInsets))
}
}
You will not be able to render it live in Interface Builder though.

Adding space/padding to a UILabel

I have a UILabel where I want to add space in the top and in the bottom. With the minimum height in constraints, I've modified it to:
To do this I've used:
override func drawTextInRect(rect: CGRect) {
var insets: UIEdgeInsets = UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 10.0)
super.drawTextInRect(UIEdgeInsetsInsetRect(rect, insets))
}
But I've to find a different method because if I write more than two lines, the problem is the same:
I have tried with it on Swift 4.2, hopefully it work for you!
#IBDesignable class PaddingLabel: UILabel {
#IBInspectable var topInset: CGFloat = 5.0
#IBInspectable var bottomInset: CGFloat = 5.0
#IBInspectable var leftInset: CGFloat = 7.0
#IBInspectable var rightInset: CGFloat = 7.0
override func drawText(in rect: CGRect) {
let insets = UIEdgeInsets(top: topInset, left: leftInset, bottom: bottomInset, right: rightInset)
super.drawText(in: rect.inset(by: insets))
}
override var intrinsicContentSize: CGSize {
let size = super.intrinsicContentSize
return CGSize(width: size.width + leftInset + rightInset,
height: size.height + topInset + bottomInset)
}
override var bounds: CGRect {
didSet {
// ensures this works within stack views if multi-line
preferredMaxLayoutWidth = bounds.width - (leftInset + rightInset)
}
}
}
Or you can use CocoaPods here https://github.com/levantAJ/PaddingLabel
pod 'PaddingLabel', '1.2'
If you want to stick with UILabel, without subclassing it, Mundi has given you a clear solution.
If alternatively, you would be willing to avoid wrapping the UILabel with a UIView, you could use UITextView to enable the use of UIEdgeInsets (padding) or subclass UILabel to support UIEdgeInsets.
Using a UITextView would only need to provide the insets (Objective-C):
textView.textContainerInset = UIEdgeInsetsMake(10, 0, 10, 0);
Alternative, if you subclass UILabel, an example to this approach would be overriding the drawTextInRect method
(Objective-C)
- (void)drawTextInRect:(CGRect)uiLabelRect {
UIEdgeInsets myLabelInsets = {10, 0, 10, 0};
[super drawTextInRect:UIEdgeInsetsInsetRect(uiLabelRect, myLabelInsets)];
}
You could additionally provide your new subclassed UILabel with insets variables for TOP, LEFT, BOTTOM and RIGHT.
An example code could be:
In .h (Objective-C)
float topInset, leftInset,bottomInset, rightInset;
In .m (Objective-C)
- (void)drawTextInRect:(CGRect)uiLabelRect {
[super drawTextInRect:UIEdgeInsetsInsetRect(uiLabelRect, UIEdgeInsetsMake(topInset,leftInset,bottomInset,rightInset))];
}
From what I have seen, it seems you have to override the intrinsicContentSize of the UILabel when subclassing it.
So you should override intrinsicContentSize like:
- (CGSize) intrinsicContentSize {
CGSize intrinsicSuperViewContentSize = [super intrinsicContentSize] ;
intrinsicSuperViewContentSize.height += topInset + bottomInset ;
intrinsicSuperViewContentSize.width += leftInset + rightInset ;
return intrinsicSuperViewContentSize ;
}
And add the following method to edit your insets, instead of editing them individually:
- (void) setContentEdgeInsets:(UIEdgeInsets)edgeInsets {
topInset = edgeInsets.top;
leftInset = edgeInsets.left;
rightInset = edgeInsets.right;
bottomInset = edgeInsets.bottom;
[self invalidateIntrinsicContentSize] ;
}
It will update the size of your UILabel to match the edge insets and cover the multiline necessity you referred to.
After searching a bit I have found this Gist with an IPInsetLabel. If none of those solutions work you could try it out.
There was a similar question (duplicate) about this matter.
For a full list of available solutions, see this answer: UILabel text margin
You can do it properly from IB :
change the text to attributed
go to dropdown list with "..."
you will see some padding properties for the lines, paragraphs and text change indent first line or anything you want
check the result
Swift 3
import UIKit
class PaddingLabel: UILabel {
#IBInspectable var topInset: CGFloat = 5.0
#IBInspectable var bottomInset: CGFloat = 5.0
#IBInspectable var leftInset: CGFloat = 5.0
#IBInspectable var rightInset: CGFloat = 5.0
override func drawText(in rect: CGRect) {
let insets = UIEdgeInsets(top: topInset, left: leftInset, bottom: bottomInset, right: rightInset)
super.drawText(in: UIEdgeInsetsInsetRect(rect, insets))
}
override var intrinsicContentSize: CGSize {
get {
var contentSize = super.intrinsicContentSize
contentSize.height += topInset + bottomInset
contentSize.width += leftInset + rightInset
return contentSize
}
}
}
Just use a UIButton, its already built in. Turn off all the extra button features and you have a label that you can set edge instets on.
let button = UIButton()
button.contentEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
button.setTitle("title", for: .normal)
button.tintColor = .white // this will be the textColor
button.isUserInteractionEnabled = false
Just use a UIView as a superview and define a fixed margin to the label with auto layout.
SWIFT 4
Easy to use solution, available for all UILabel child in project.
Example:
let label = UILabel()
label.<Do something>
label.padding = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 0)
UILabel Extension
import UIKit
extension UILabel {
private struct AssociatedKeys {
static var padding = UIEdgeInsets()
}
public var padding: UIEdgeInsets? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.padding) as? UIEdgeInsets
}
set {
if let newValue = newValue {
objc_setAssociatedObject(self, &AssociatedKeys.padding, newValue as UIEdgeInsets?, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
override open func draw(_ rect: CGRect) {
if let insets = padding {
self.drawText(in: rect.inset(by: insets))
} else {
self.drawText(in: rect)
}
}
override open var intrinsicContentSize: CGSize {
guard let text = self.text else { return super.intrinsicContentSize }
var contentSize = super.intrinsicContentSize
var textWidth: CGFloat = frame.size.width
var insetsHeight: CGFloat = 0.0
var insetsWidth: CGFloat = 0.0
if let insets = padding {
insetsWidth += insets.left + insets.right
insetsHeight += insets.top + insets.bottom
textWidth -= insetsWidth
}
let newSize = text.boundingRect(with: CGSize(width: textWidth, height: CGFloat.greatestFiniteMagnitude),
options: NSStringDrawingOptions.usesLineFragmentOrigin,
attributes: [NSAttributedString.Key.font: self.font], context: nil)
contentSize.height = ceil(newSize.size.height) + insetsHeight
contentSize.width = ceil(newSize.size.width) + insetsWidth
return contentSize
}
}
We finally figured a full and correct solution which works in all cases, including stack views, dynamic cells, dynamic number of lines, collection views, animated padding, every character count, and every other situation.
Padding a UILabel, full solution. Updated for 2021.
It turns out there are three things that must be done.
1. Must call textRect#forBounds with the new smaller size
2. Must override drawText with the new smaller size
3. If a dynamically sized cell, must adjust intrinsicContentSize
In the typical example below, the text unit is in a table view, stack view or similar construction, which gives it a fixed width. In the example we want padding of 60,20,20,24.
Thus, we take the "existing" intrinsicContentSize and actually add 80 to the height.
To repeat ...
You have to literally "get" the height calculated "so far" by the engine, and change that value.
I find that process confusing, but, that is how it works. For me, Apple should expose a call named something like "preliminary height calculation".
Secondly we have to actually use the textRect#forBounds call with our new smaller size.
So in textRect#forBounds we first make the size smaller and then call super.
Alert! You must call super after, not before!
If you carefully investigate all the attempts and discussion on this page, that is the exact problem.
Notice some solutions "seem to usually work". This is indeed the exact reason - confusingly you must "call super afterwards", not before.
If you call super "in the wrong order", it usually works, but fails for certain specific text lengths.
Here is an exact visual example of "incorrectly doing super first":
Notice the 60,20,20,24 margins are correct BUT the size calculation is actually wrong, because it was done with the "super first" pattern in textRect#forBounds.
Fixed:
Only now does the textRect#forBounds engine know how to do the calculation properly:
Finally!
Again, in this example the UILabel is being used in the typical situation where width is fixed. So in intrinsicContentSize we have to "add" the overall extra height we want. (You don't need to "add" in any way to the width, that would be meaningless as it is fixed.)
Then in textRect#forBounds you get the bounds "suggested so far" by autolayout, you subtract your margins, and only then call again to the textRect#forBounds engine, that is to say in super, which will give you a result.
Finally and simply in drawText you of course draw in that same smaller box.
Phew!
let UIEI = UIEdgeInsets(top: 60, left: 20, bottom: 20, right: 24) // as desired
override var intrinsicContentSize:CGSize {
numberOfLines = 0 // don't forget!
var s = super.intrinsicContentSize
s.height = s.height + UIEI.top + UIEI.bottom
s.width = s.width + UIEI.left + UIEI.right
return s
}
override func drawText(in rect:CGRect) {
let r = rect.inset(by: UIEI)
super.drawText(in: r)
}
override func textRect(forBounds bounds:CGRect,
limitedToNumberOfLines n:Int) -> CGRect {
let b = bounds
let tr = b.inset(by: UIEI)
let ctr = super.textRect(forBounds: tr, limitedToNumberOfLines: 0)
// that line of code MUST be LAST in this function, NOT first
return ctr
}
Once again. Note that the answers on this and other QA that are "almost" correct suffer the problem in the first image above - the "super is in the wrong place". You must force the size bigger in intrinsicContentSize and then in textRect#forBounds you must first shrink the first-suggestion bounds and then call super.
Summary: you must "call super last" in textRect#forBounds
That's the secret.
Note that you do not need to and should not need to additionally call invalidate, sizeThatFits, needsLayout or any other forcing call. A correct solution should work properly in the normal autolayout draw cycle.
Without Storyboard:
class PaddingLabel: UILabel {
var topInset: CGFloat
var bottomInset: CGFloat
var leftInset: CGFloat
var rightInset: CGFloat
required init(withInsets top: CGFloat, _ bottom: CGFloat,_ left: CGFloat,_ right: CGFloat) {
self.topInset = top
self.bottomInset = bottom
self.leftInset = left
self.rightInset = right
super.init(frame: CGRect.zero)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func drawText(in rect: CGRect) {
let insets = UIEdgeInsets(top: topInset, left: leftInset, bottom: bottomInset, right: rightInset)
super.drawText(in: UIEdgeInsetsInsetRect(rect, insets))
}
override var intrinsicContentSize: CGSize {
get {
var contentSize = super.intrinsicContentSize
contentSize.height += topInset + bottomInset
contentSize.width += leftInset + rightInset
return contentSize
}
}
}
Usage:
let label = PaddingLabel(8, 8, 16, 16)
label.font = .boldSystemFont(ofSize: 16)
label.text = "Hello World"
label.backgroundColor = .black
label.textColor = .white
label.textAlignment = .center
label.layer.cornerRadius = 8
label.clipsToBounds = true
label.sizeToFit()
view.addSubview(label)
Result:
Swift 4+
class EdgeInsetLabel: UILabel {
var textInsets = UIEdgeInsets.zero {
didSet { invalidateIntrinsicContentSize() }
}
override func textRect(forBounds bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect {
let textRect = super.textRect(forBounds: bounds, 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))
}
}
Usage:
let label = EdgeInsetLabel()
label.textInsets = UIEdgeInsets(top: 2, left: 6, bottom: 2, right: 6)
Just use autolayout:
let paddedWidth = myLabel.intrinsicContentSize.width + 2 * padding
myLabel.widthAnchor.constraint(equalToConstant: paddedWidth).isActive = true
Done.
Swift 3 Code with Implementation Example
class UIMarginLabel: UILabel {
var topInset: CGFloat = 0
var rightInset: CGFloat = 0
var bottomInset: CGFloat = 0
var leftInset: CGFloat = 0
override func drawText(in rect: CGRect) {
let insets: UIEdgeInsets = UIEdgeInsets(top: self.topInset, left: self.leftInset, bottom: self.bottomInset, right: self.rightInset)
self.setNeedsLayout()
return super.drawText(in: UIEdgeInsetsInsetRect(rect, insets))
}
}
class LabelVC: UIViewController {
//Outlets
#IBOutlet weak var labelWithMargin: UIMarginLabel!
override func viewDidLoad() {
super.viewDidLoad()
//Label settings.
labelWithMargin.leftInset = 10
view.layoutIfNeeded()
}
}
Don't forget to add class name UIMarginLabel in storyboard label object.
Happy Coding!
In Swift 3
best and simple way
class UILabelPadded: UILabel {
override func drawText(in rect: CGRect) {
let insets = UIEdgeInsets.init(top: 0, left: 5, bottom: 0, right: 5)
super.drawText(in: UIEdgeInsetsInsetRect(rect, insets))
}
}
As per Swift 4.2 (Xcode 10 beta 6) "UIEdgeInsetsInsetRect" being deprecated.
I've also declared the class public to make it more useful.
public class UIPaddedLabel: UILabel {
#IBInspectable var topInset: CGFloat = 5.0
#IBInspectable var bottomInset: CGFloat = 5.0
#IBInspectable var leftInset: CGFloat = 7.0
#IBInspectable var rightInset: CGFloat = 7.0
public override func drawText(in rect: CGRect) {
let insets = UIEdgeInsets.init(top: topInset, left: leftInset, bottom: bottomInset, right: rightInset)
super.drawText(in: rect.inset(by: insets))
}
public override var intrinsicContentSize: CGSize {
let size = super.intrinsicContentSize
return CGSize(width: size.width + leftInset + rightInset,
height: size.height + topInset + bottomInset)
}
public override func sizeToFit() {
super.sizeThatFits(intrinsicContentSize)
}
}
I edited a little in the accepted answer. There is a problem when leftInset and rightInset increase, a part of text will be disappeared, b/c the width of label will be narrowed but the height does not increase as figure:
To resolve this problem you need to re-calculate height of text as follow:
#IBDesignable class PaddingLabel: UILabel {
#IBInspectable var topInset: CGFloat = 20.0
#IBInspectable var bottomInset: CGFloat = 20.0
#IBInspectable var leftInset: CGFloat = 20.0
#IBInspectable var rightInset: CGFloat = 20.0
override func drawTextInRect(rect: CGRect) {
let insets = UIEdgeInsets(top: topInset, left: leftInset, bottom: bottomInset, right: rightInset)
super.drawTextInRect(UIEdgeInsetsInsetRect(rect, insets))
}
override func intrinsicContentSize() -> CGSize {
var intrinsicSuperViewContentSize = super.intrinsicContentSize()
let textWidth = frame.size.width - (self.leftInset + self.rightInset)
let newSize = self.text!.boundingRectWithSize(CGSizeMake(textWidth, CGFloat.max), options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: [NSFontAttributeName: self.font], context: nil)
intrinsicSuperViewContentSize.height = ceil(newSize.size.height) + self.topInset + self.bottomInset
return intrinsicSuperViewContentSize
}
}
and result:
I hope to help some people in the same situation as me.
Another option without subclassing would be to:
Set label text
sizeToFit()
then increase label height a little to simulate padding
label.text = "someText"
label.textAlignment = .center
label.sizeToFit()
label.frame = CGRect( x: label.frame.x, y: label.frame.y,width: label.frame.width + 20,height: label.frame.height + 8)
Just like the other answers, but it fixes a bug:
When label.width is controlled by auto layout, sometimes text will be cropped.
#IBDesignable
class InsetLabel: UILabel {
#IBInspectable var topInset: CGFloat = 4.0
#IBInspectable var leftInset: CGFloat = 4.0
#IBInspectable var bottomInset: CGFloat = 4.0
#IBInspectable var rightInset: CGFloat = 4.0
var insets: UIEdgeInsets {
get {
return UIEdgeInsets.init(top: topInset, left: leftInset, bottom: bottomInset, right: rightInset)
}
set {
topInset = newValue.top
leftInset = newValue.left
bottomInset = newValue.bottom
rightInset = newValue.right
}
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
var adjSize = super.sizeThatFits(size)
adjSize.width += leftInset + rightInset
adjSize.height += topInset + bottomInset
return adjSize
}
override var intrinsicContentSize: CGSize {
let systemContentSize = super.intrinsicContentSize
let adjustSize = CGSize(width: systemContentSize.width + leftInset + rightInset, height: systemContentSize.height + topInset + bottomInset)
if adjustSize.width > preferredMaxLayoutWidth && preferredMaxLayoutWidth != 0 {
let constraintSize = CGSize(width: bounds.width - (leftInset + rightInset), height: .greatestFiniteMagnitude)
let newSize = super.sizeThatFits(constraintSize)
return CGSize(width: systemContentSize.width, height: ceil(newSize.height) + topInset + bottomInset)
} else {
return adjustSize
}
}
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: insets))
}
}
Swift 3, iOS10 solution:
open class UIInsetLabel: UILabel {
open var insets : UIEdgeInsets = UIEdgeInsets() {
didSet {
super.invalidateIntrinsicContentSize()
}
}
open override var intrinsicContentSize: CGSize {
var size = super.intrinsicContentSize
size.width += insets.left + insets.right
size.height += insets.top + insets.bottom
return size
}
override open func drawText(in rect: CGRect) {
return super.drawText(in: UIEdgeInsetsInsetRect(rect, insets))
}
}
Swift 5 Example with UILabel Extension
With the code below setting your margins is as easy as label.setMargins(15).
extension UILabel {
func setMargins(_ margin: CGFloat = 10) {
if let textString = self.text {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.firstLineHeadIndent = margin
paragraphStyle.headIndent = margin
paragraphStyle.tailIndent = -margin
let attributedString = NSMutableAttributedString(string: textString)
attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: attributedString.length))
attributedText = attributedString
}
}
}
Subclass UILabel. (File-New-File- CocoaTouchClass-make Subclass of UILabel).
// sampleLabel.swift
import UIKit
class sampleLabel: UILabel {
let topInset = CGFloat(5.0), bottomInset = CGFloat(5.0), leftInset = CGFloat(8.0), rightInset = CGFloat(8.0)
override func drawTextInRect(rect: CGRect) {
let insets: UIEdgeInsets = UIEdgeInsets(top: topInset, left: leftInset, bottom: bottomInset, right: rightInset)
super.drawTextInRect(UIEdgeInsetsInsetRect(rect, insets))
}
override func intrinsicContentSize() -> CGSize {
var intrinsicSuperViewContentSize = super.intrinsicContentSize()
intrinsicSuperViewContentSize.height += topInset + bottomInset
intrinsicSuperViewContentSize.width += leftInset + rightInset
return intrinsicSuperViewContentSize
}
}
On ViewController:
override func viewDidLoad() {
super.viewDidLoad()
let labelName = sampleLabel(frame: CGRectMake(0, 100, 300, 25))
labelName.text = "Sample Label"
labelName.backgroundColor = UIColor.grayColor()
labelName.textColor = UIColor.redColor()
labelName.shadowColor = UIColor.blackColor()
labelName.font = UIFont(name: "HelveticaNeue", size: CGFloat(22))
self.view.addSubview(labelName)
}
OR Associate custom UILabel class on Storyboard as Label's class.
If you want to use UILabel
class UILabel : UIKit.UILabel {
var insets = UIEdgeInsets.zero {
didSet { invalidateIntrinsicContentSize() }
}
override func textRect(forBounds bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect {
let textRect = super.textRect(forBounds: bounds, limitedToNumberOfLines: numberOfLines)
let invertedInsets = UIEdgeInsets(top: -insets.top,
left: -insets.left,
bottom: -insets.bottom,
right: -insets.right)
return textRect.inset(by: invertedInsets)
}
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: insets))
}
}
An elaboration on Mundi's answer.
I.e., embedding a label in a UIView and enforcing padding through Auto Layout. Example:
Overview:
Create a UIView ("panel"), and set its appearance.
Create a UILabel and add it to the panel.
Add constraints to enforce padding.
Add the panel to your view hierarchy, and then position the panel.
Details:
Create the panel view.
let panel = UIView()
panel.backgroundColor = .green
panel.layer.cornerRadius = 12
Create the label, add it to the panel as a subview.
let label = UILabel()
panel.addSubview(label)
Add constraints between the edges of the label and the panel. This forces the panel to keep a distance from the label. I.e., "padding".
Editorial: doing all this by hand is super-tedious, verbose and error-prone. I suggest you pick an Auto Layout wrapper from GitHub or write one yourself
label.panel.translatesAutoresizingMaskIntoConstraints = false
label.topAnchor.constraint(equalTo: panel.topAnchor,
constant: vPadding).isActive = true
label.bottomAnchor.constraint(equalTo: panel.bottomAnchor,
constant: -vPadding).isActive = true
label.leadingAnchor.constraint(equalTo: panel.leadingAnchor,
constant: hPadding).isActive = true
label.trailingAnchor.constraint(equalTo: panel.trailingAnchor,
constant: -hPadding).isActive = true
label.textAlignment = .center
Add the panel to your view hierarchy and then add positioning constraints. E.g., hug the right-hand side of a tableViewCell, as in the example image.
Note: you only need to add positional constraints, not dimensional constraints: Auto Layout will solve the layout based on both the intrinsicContentSize of the label and the constraints added earlier.
hostView.addSubview(panel)
panel.translatesAutoresizingMaskIntoConstraints = false
panel.trailingAnchor.constraint(equalTo: hostView.trailingAnchor,
constant: -16).isActive = true
panel.centerYAnchor.constraint(equalTo: hostView.centerYAnchor).isActive = true
Use this code if you are facing a text trimming problem while applying padding.
#IBDesignable class PaddingLabel: UILabel {
#IBInspectable var topInset: CGFloat = 5.0
#IBInspectable var bottomInset: CGFloat = 5.0
#IBInspectable var leftInset: CGFloat = 5.0
#IBInspectable var rightInset: CGFloat = 5.0
override func drawText(in rect: CGRect) {
let insets = UIEdgeInsets.init(top: topInset, left: leftInset, bottom: bottomInset, right: rightInset)
super.drawText(in: UIEdgeInsetsInsetRect(rect, insets))
}
override var intrinsicContentSize: CGSize {
var intrinsicSuperViewContentSize = super.intrinsicContentSize
let textWidth = frame.size.width - (self.leftInset + self.rightInset)
let newSize = self.text!.boundingRect(with: CGSize(textWidth, CGFloat.greatestFiniteMagnitude), options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: [NSFontAttributeName: self.font], context: nil)
intrinsicSuperViewContentSize.height = ceil(newSize.size.height) + self.topInset + self.bottomInset
return intrinsicSuperViewContentSize
}
}
extension CGSize{
init(_ width:CGFloat,_ height:CGFloat) {
self.init(width:width,height:height)
}
}
Strictly for SINGLE-LINE labels: (2021 syntax)
For anyone googling here who needs padding on a STRICTLY SINGLE LINE label (such as a section heading or other list item),
The syntax has changed a lot. Beware out of date info on the internet.
Here's the exact class to copy and paste:
// add 100 above, 50 padding below a SINGLE-LINE label
import UIKit
class SingleLineLabelWithSpacing: UILabel {
// STRICTLY for SINGLE LINE labels
// only works with SINGLE LINE labels
override func drawText(in rect: CGRect) {
let insets: UIEdgeInsets = UIEdgeInsets(
top: 100, left: 0, bottom: 50, right: 0)
super.drawText(in: rect.inset(by: insets))
}
override var intrinsicContentSize: CGSize {
var ic = super.intrinsicContentSize
ic.height = ic.height + 150
return ic
}
}
In the example, padding above/below of 100/50.
This is the usual thing to do when you have any sort of scrolling list, feed, or other list.
In this way you never have to think about the spacing above/below the headline, username, etc - you just drop it in the stack view or whatever the case is.
Also of course you can change the two values everywhere all at once when the designers want to tweak it.
Reminder: if you want to truly pad a UILabel so that it works perfectly regardless of number of lines of text, dynamic sizing cells, animations, etc etc etc, it is very complicated. The only correct answer is: https://stackoverflow.com/a/58876988/294884
Easy padding (Swift 3.0, Alvin George answer):
class NewLabel: UILabel {
override func textRect(forBounds bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect {
return self.bounds.insetBy(dx: CGFloat(15.0), dy: CGFloat(15.0))
}
override func draw(_ rect: CGRect) {
super.drawText(in: self.bounds.insetBy(dx: CGFloat(5.0), dy: CGFloat(5.0)))
}
}
One pragmatic solution is to add blank labels of the same height and color as the main label. Set the leading/trailing space to the main label to zero, align vertical centers, and make the width your desired margin.
If you don't want or need to use an #IBInspectable / #IBDesignable UILabel in Storyboard (I think those are rendered too slow anyway), then it is cleaner to use UIEdgeInsets instead of 4 different CGFloats.
Code example for Swift 4.2:
class UIPaddedLabel: UILabel {
var padding = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
public override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: padding))
}
public override var intrinsicContentSize: CGSize {
let size = super.intrinsicContentSize
return CGSize(width: size.width + padding.left + padding.right,
height: size.height + padding.top + padding.bottom)
}
}
My solution is similar to what people answered but adds sizeThatFits to help UIKit to figure out the right size.
class InsetLabel : UILabel {
#objc var textInsets: UIEdgeInsets = .zero
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: textInsets))
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
var s = super.sizeThatFits(CGSize(width: size.width - (textInsets.left + textInsets.right), height: size.height - (textInsets.top + textInsets.bottom)))
s.height += textInsets.top + textInsets.bottom
return s
}
}
Similar to other answers, but with a func class to setup the padding dinamically:
class UILabelExtendedView: UILabel
{
var topInset: CGFloat = 4.0
var bottomInset: CGFloat = 4.0
var leftInset: CGFloat = 8.0
var rightInset: CGFloat = 8.0
override func drawText(in rect: CGRect)
{
let insets: UIEdgeInsets = UIEdgeInsets(top: topInset, left: leftInset, bottom: bottomInset, right: rightInset)
super.drawText(in: UIEdgeInsetsInsetRect(rect, insets))
}
override public var intrinsicContentSize: CGSize
{
var contentSize = super.intrinsicContentSize
contentSize.height += topInset + bottomInset
contentSize.width += leftInset + rightInset
return contentSize
}
func setPadding(top: CGFloat, left: CGFloat, bottom: CGFloat, right: CGFloat){
self.topInset = top
self.bottomInset = bottom
self.leftInset = left
self.rightInset = right
let insets: UIEdgeInsets = UIEdgeInsets(top: top, left: left, bottom: bottom, right: right)
super.drawText(in: UIEdgeInsetsInsetRect(self.frame, insets))
}
}
If you want to add 2px padding around the textRect, just do this:
let insets = UIEdgeInsets(top: -2, left: -2, bottom: -2, right: -2)
label.frame = UIEdgeInsetsInsetRect(textRect, insets)

Resources