Here, I am trying to have a label with some padding (left, right, top and bottom) around the text.
This issue has related post on SOF and after reading a few of them, I tried using a solution proposed here:
This is the code for my subclassing UILabel:
import UIKit
class LuxLabel: UILabel {
//let padding: UIEdgeInsets
var padding: UIEdgeInsets = UIEdgeInsets.zero {
didSet {
self.invalidateIntrinsicContentSize()
}
}
// Create a new PaddingLabel instance programamtically with the desired insets
required init(padding: UIEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10)) {
self.padding = padding
super.init(frame: CGRect.zero)
}
// Create a new PaddingLabel instance programamtically with default insets
override init(frame: CGRect) {
padding = UIEdgeInsets.zero // set desired insets value according to your needs
super.init(frame: frame)
}
// Create a new PaddingLabel instance from Storyboard with default insets
required init?(coder aDecoder: NSCoder) {
padding = UIEdgeInsets.zero // set desired insets value according to your needs
super.init(coder: aDecoder)
}
override func drawText(in rect: CGRect) {
super.drawText(in: UIEdgeInsetsInsetRect(rect, padding))
}
// Override `intrinsicContentSize` property for Auto layout code
override var intrinsicContentSize: CGSize {
let superContentSize = super.intrinsicContentSize
let width = superContentSize.width + padding.left + padding.right
let heigth = superContentSize.height + padding.top + padding.bottom
return CGSize(width: width, height: heigth)
}
}
It is based on PaddingLabel (cf. the above link).
It is mostly working well, but for some reasons that I do not understand, there are cases where things go wrong and the display gets truncated.
This is an example:
The string to put on the label is:
"It has a square shape and a blue color."
The code to create the label is:
let label = LuxLabel(padding: UIEdgeInsets(top: 5, left: 10, bottom: 5, right: 10))
label.numberOfLines = 0
and this is the result:
If I add this line to the two above:
label.lineBreakMode = .byWordWrapping
the result is:
I have also set some constraints. All this works 95% of the time. Can anyone see what is the problem?
Try calling invalidateIntrinsicContentSize:
var padding: UIEdgeInsets = UIEdgeInsets.zero {
didSet {
self.invalidateIntrinsicContentSize()
}
}
EDIT:
I have tried different options. If you update the frame size with the intrinsicContentSize in layoutSubviews that make the trick but I don't know if there is a better way to do it:
override func layoutSubviews() {
super.layoutSubviews()
self.frame.size = self.intrinsicContentSize
}
Related
I am very new to autolayout and I have been struggling with one thing.
The Roll btn in the BottomView - I am trying to increase it's height according to the text size. As you can see, the height of the textlabel of btn in increasing, but the height of btn is not.
The constraints that I have applied:
1)Horizontallly and verticallly centered in superview
2)Leading and trailing from superview
3) I tried setting height constraint to greaterThanEqualTo a constant also. But that didnt work
4)I tried SizeToFit also. Didn't work
5)I have set link lineBreak as WordWrap and number of lines in titleLabel as 0. Still its not working
i used this solution to change height of uilabal dynamically
you can get this extension to get height and width of string
extension String {
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
return ceil(boundingBox.height)
}
func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
return ceil(boundingBox.width)
}
}
since you are using storyboard set height and width constant then link them to viewcontroller script
YourContrainsHeight.constant = SomeString.height
YourContrainsWidth.constant = SomeString.width
When you modify the .titleLabel of a UIButton, auto-layout doesn't take your changes into account.
To get a properly auto-sizing multi-line button, you need to subclass it and return a valid .intrinsicContentSize.
Here is a quick example:
#IBDesignable
class MultilineButton: UIButton {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
commonInit()
setNeedsLayout()
}
func commonInit() -> Void {
self.titleLabel?.numberOfLines = 0
self.titleLabel?.textAlignment = .center
self.contentEdgeInsets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0)
}
override var intrinsicContentSize: CGSize {
let size = self.titleLabel!.intrinsicContentSize
return CGSize(width: size.width + contentEdgeInsets.left + contentEdgeInsets.right, height: size.height + contentEdgeInsets.top + contentEdgeInsets.bottom)
}
override func layoutSubviews() {
super.layoutSubviews()
titleLabel?.preferredMaxLayoutWidth = self.titleLabel!.frame.size.width
}
}
The class is marked #IBDesignable so you can use it in Storyboard / Interface Builder get accurate results.
Here's a look (in Storyboard) of 3 instances of this button. First with a short title, so it doesn't need to wrap; next with a title that does wrap; and third with a title that has embedded newlines:
Note that this example explicitly sets the Content Edge Insets to 8-pts on all 4 sides. If you want to be able to change that for individual buttons, it will need a little editing.
I have set my UILabel padding using StackOverflow's popular thread for resolving auto-layout issue.
This thread is basically a UILabel extension.
Part of the answer is:
class NRLabel : UILabel {
var textInsets = UIEdgeInsets.zero {
didSet { invalidateIntrinsicContentSize() }
}
override func textRect(forBounds bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect {
let insetRect = UIEdgeInsetsInsetRect(bounds, 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 UIEdgeInsetsInsetRect(textRect, invertedInsets)
}
override func drawText(in rect: CGRect) {
super.drawText(in: UIEdgeInsetsInsetRect(rect, textInsets))
}
}
Everything is working fine, padding has been added but i need to reload the tableViewcell to see the effect.
I have overridden my customCell's viewWillLayoutSubviews() function like this for padding
self.chatLabel.textInsets = UIEdgeInsets.init(top: 10, left: 10, bottom: 10, right: 10)
And the effect is like this.
You see, first label is getting it's padding after reloading the cell.
Pls suggest how to achieve UILabel padding by using the extension mentioned above so that this issue is resolved.
Add
this two function into your extention and remove all other:
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 var intrinsicContentSize : CGSize {
var intrinsicSuperViewContentSize = super.intrinsicContentSize
intrinsicSuperViewContentSize.height += topInset + bottomInset
intrinsicSuperViewContentSize.width += leftInset + rightInset
return intrinsicSuperViewContentSize
}
Using your NRLabel class, this works fine for me. The theLabel is added in Storyboard TableViewCell Prototype, with constraints set to allow auto-sizing rows.
class PaddedLabelCell : UITableViewCell {
#IBOutlet weak var theLabel: NRLabel!
override func awakeFromNib() {
super.awakeFromNib()
self.setupLabel()
}
func setupLabel() -> Void {
theLabel.layer.cornerRadius = 8.0
theLabel.layer.masksToBounds = true
theLabel.textInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
// any other setup stuff can go here....
}
}
You have nothing in NRLabel which tells it to redraw when the textInsets property is changed. You need to do something like this:
var textInsets = UIEdgeInsets.zero {
didSet {
invalidateIntrinsicContentSize()
setNeedsDisplay()
}
}
So now when the textInsets property is changed the NRLabel will be re-drawn with the new text insets.
After some long tiring hours i just found the solution which i never tried and i don't know why. :(
In cellForRowAtIndexPath i just wrote the line which was in customCell's file.
Just call this line before returning cell.
cell.chatLabel.textInsets = UIEdgeInsets.init(top: 10, left: 10, bottom: 10, right: 10)
The reason behind it may be, Cell is drawn with all the functionality but it didn't get much to refresh it's UI.
So calling the line just before showing the cell resolve the problem.
I have created an iOS application using Swift. Now I have one issue I want to make my text center + bottom in one label and center + top in another label. Is it possible to do?
I tried this
//Here learnItem is my label.
let constraintSize: CGSize = CGSizeMake(learnItem.frame.size.width, CGFloat(MAXFLOAT))
let textRect: CGRect = learnItem.text!.boundingRectWithSize(constraintSize, options: .UsesLineFragmentOrigin, attributes: [NSFontAttributeName: learnItem.font], context: nil)
learnItem.drawRect(textRect);
It doesn't affect anything. So maybe it is a wromg code. Also I saw
learnItem.textAlignment = .Center
There is option for center, justified, left, natural, right. But that is not what I want.
Please some one help me how to make my text center + bottom and center + top?
In order to do this you should override drawTextInRect method.
// MARK: - BottomAlignedLabel
#IBDesignable class BottomAlignedLabel: UILabel {
// MARK: Lifecycle
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func drawText(in rect: CGRect) {
guard text != nil else {
return super.drawText(in: rect)
}
let height = self.sizeThatFits(rect.size).height
let y = rect.origin.y + rect.height - height
super.drawText(in: CGRect(x: 0, y: y, width: rect.width, height: height))
}
}
For top alignment y should be 0.
If you're using storyboard the easiest way to use this code is just change class for your label in storyboard.
I am an Android developer learning iOS. How do you add padding (inset) to UILabel in iOS Swift? I have not been successful at finding an easy tutorial on this. Thank you.
Also, how can I add margins to UI elements in Swift? When I try the below, nothing gets updated (upon hitting "add constraint").
This is the code you need to add in Swift 3:
class InsetLabel: UILabel {
let topInset = CGFloat(0)
let bottomInset = CGFloat(0)
let leftInset = CGFloat(20)
let rightInset = CGFloat(20)
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 intrinsicSuperViewContentSize = super.intrinsicContentSize
intrinsicSuperViewContentSize.height += topInset + bottomInset
intrinsicSuperViewContentSize.width += leftInset + rightInset
return intrinsicSuperViewContentSize
}
}
Probably the best idea is to subclass UILabel and override drawTextInRect: like this:
class InsetLabel: UILabel {
override func drawTextInRect(rect: CGRect) {
super.drawTextInRect(UIEdgeInsetsInsetRect(rect, UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)))
}
}
SWIFT 4+:
class InsetLabel: UILabel {
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)))
}
}
need add this:
class InsetLabel: UILabel {
let topInset = CGFloat(12.0), bottomInset = CGFloat(12.0), leftInset = CGFloat(12.0), rightInset = CGFloat(12.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
}
}
There is a solution simply using the storyboard. In the storyboard, create a UIView object whose purpose will be to surround your label in order to give it padding. Then, also from the storyboard, put your UILabel inside of that view. Now select the UILabel object and bring up the Add New Constraints popup by clicking the "Pin" icon. If you want a padding of 5 all the way around the UILabel, click each of the 4 red I-beams in the Add New Constraints popup, and THEN type 5 in each of the 4 boxes adjacent to the I-beams. If you type 5 first and then click an I-Beam, XCode reverts to whatever value was in the box before you typed 5. Finally click the Add Constraints button.
In the Add Constraints popup, XCode ignores top, left, right, or bottom constraint values for which you have not clicked the corresponding red I-beam. This probably answers the second part of your question.
Remember also to setup the constraints of the UIView you added. If you don't define where it goes in the storyboard (e.g., pinned to the top and left of the main view of the view controller), it will not be assigned a default location, and it may not show up at all when you run your app.
I have tried with it on Swift 4.2, hopefully, it works 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.init(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)
}
}
Or
you can use CocoaPods here https://github.com/levantAJ/PaddingLabel
pod 'PaddingLabel', '1.1'
Another way would be to override textRect:forBounds:limitedToNumberOfLines:
Returns the drawing rectangle for the label’s text. Override this
method in subclasses that require changes in the label’s bounding
rectangle to occur before the system performs other text layout
calculations. Use the value in the numberOfLines parameter to limit
the height of the returned rectangle to the specified number of lines
of text. This method may be called by the system if there was a prior
call to the sizeToFit() or sizeThatFits(_:) method. Note that labels
in UITableViewCell objects are sized based on cell dimensions, and not
on a requested size.
public class JPSLabel : UILabel
{
open var textInsets: UIEdgeInsets = UIEdgeInsets.zero
override public func textRect(forBounds bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect
{
var insetBounds = super.textRect(forBounds: bounds, limitedToNumberOfLines: numberOfLines)
insetBounds.size.width += self.textInsets.left + self.textInsets.right
insetBounds.size.height += self.textInsets.top + self.textInsets.bottom
return insetBounds
}
}
I have tested below codes to my app, Swift 5.0 +
class CustomLabel: 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))
}
}
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:
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?
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.
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))
}
}
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));
}
}
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))
}}
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()
}
}
...
}
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))
}
}
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:
Set a width constraint for the UILabel
Connect the constraint via IBOutlet onto your code, either VC (custom cell class if you're doing an expanding table view cell)
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.
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
}