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
}
Related
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
}
Below is an image of some UITextFields. Regarding the large one on the bottom, how do I get it to start text at the top left (not the middle), and how do I get them all to start a bit to the right. As you can see, they are awkwardly close to the left edge.
For textField, override following methods:
class InsetTextField: UITextField {
var inset: CGFloat = 10
override func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: inset, dy: 0)
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: inset, dy: 0)
}
override func placeholderRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: inset, dy: 0)
}
}
For textView:
textView.contentInset = UIEdgeInsetsMake(inset, inset, inset, inset);
And you can be involved when the textView is done editing, check out this question.
You can sublcass to a custom TextField:
import UIKit
class CustomTextField: UITextField {
var inset:CGFloat = 12 // You can set the inset you want
override func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: inset, dy: 0)
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: inset, dy: 0)
}
}
The result, you can see the inset of the CustomTextField:
Edit
By default, UITextFields only have one line. Based off your image, I'd assume you want the user to have space to type a paragraph. For paragraphs, it is better to use UITextViews. It'd be a simpler solution for your problem.
I always use TextViews for biographies because it just makes my life much easier.
add left padding of your textfield
let paddingVie = UIView(frame: CGRect(x: 0, y:0, width: 10, height: 10))
yourtextField.leftView = paddingVie
yourtextField.leftViewMode = .always
for textview add
yourTextVieName.textContainerInset =
UIEdgeInsetsMake(8,5,8,5); // top, left, bottom, right
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've been searching for a way to make the UISlider progress bar taller, like increasing the height of the slider but couldn't find anything. I don't want to use a custom image or anything, just make it taller, so the UISlider doesn't look so thin. Is there an easy way to do this that I'm missing?
The accepted answer will undesirably change the slider's width in some cases, like if you're using a minimumValueImage and maximumValueImage. If you only want to change the height and leave everything else alone, then use this code:
override func trackRect(forBounds bounds: CGRect) -> CGRect {
var newBounds = super.trackRect(forBounds: bounds)
newBounds.size.height = 12
return newBounds
}
Here's my recent swifty implementation, building on CularBytes's ...
open class CustomSlider : UISlider {
#IBInspectable open var trackWidth:CGFloat = 2 {
didSet {setNeedsDisplay()}
}
override open func trackRect(forBounds bounds: CGRect) -> CGRect {
let defaultBounds = super.trackRect(forBounds: bounds)
return CGRect(
x: defaultBounds.origin.x,
y: defaultBounds.origin.y + defaultBounds.size.height/2 - trackWidth/2,
width: defaultBounds.size.width,
height: trackWidth
)
}
}
Use this on a UISlider in a storyboard by setting its custom class
The IBInspectable allows you to set the height from the storyboard
For those that would like to see some working code for changing the track size.
class CustomUISlider : UISlider {
override func trackRect(forBounds bounds: CGRect) -> CGRect {
//keeps original origin and width, changes height, you get the idea
let customBounds = CGRect(origin: bounds.origin, size: CGSize(width: bounds.size.width, height: 5.0))
super.trackRect(forBounds: customBounds)
return customBounds
}
//while we are here, why not change the image here as well? (bonus material)
override func awakeFromNib() {
self.setThumbImage(UIImage(named: "customThumb"), for: .normal)
super.awakeFromNib()
}
}
Only thing left is changing the class inside the storyboard:
You can keep using your seekbar action and outlet to the object type UISlider, unless you want to add some more custom stuff to your slider.
I found what I was looking for. The following method just needs to be edited in a subclass.
override func trackRect(forBounds bounds: CGRect) -> CGRect {
var customBounds = super.trackRect(forBounds: bounds)
customBounds.size.height = ...
return customBounds
}
You could play with this, see what happens:
slider.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1.0, 2.0);
Problem:
UILabel may clip italic (oblique) characters and even scripts at the left and right edges. The following screenshot displays the issue. At the left edge, the descender of the 'j' is clipped; at the right edge, the ascender of the 'l' is clipped. I realize this is subtle, and not everyone is going to care (however, the issue gets worse with larger font sizes).
Here's a less subtle example using Zapfino, size 22. Note the 'j' in jupiter looks almost like an 'i':
In the examples above, the background color of the label is orange, the text is left aligned, and the label maintains its intrinsic content size.
This is the default behavior of a UILabel and its been that way for multiple versions of iOS (so I'm not expecting a fix from Apple).
What I have tried:
Setting the label's clipsToBounds property to NO does not resolve the issue. I'm also aware that I could set a fixed width constraint on the label to give the text more room at the trailing edge. However, a fixed width constraint would not give the 'j', in the example above, more room.
I'm going to answer my own question using a solution that leverages Auto Layout and the label's alignmentRectInsets.
The top label shows the default behavior of a UILabel when the text is left aligned that the label maintains its intrinsic content size. The bottom label is a simple (almost trivial) subclass of UILabel. The bottom label does not clip the 'j' or the 'l'; instead, it gives the text some room to breathe at the left and right edges without center aligning the text (yuck).
Although the labels themselves don't appear aligned on screen, their text does appear aligned; and what's more, in IB, the labels actually have their left edges aligned because I override alignmentRectInsets in a UILabel subclass.
Here's the code that configures the two labels:
#import "ViewController.h"
#import "NonClippingLabel.h"
#interface ViewController ()
#property (weak, nonatomic) IBOutlet UILabel *topLabel;
#property (weak, nonatomic) IBOutlet NonClippingLabel *bottomLabel;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
NSString *string = #"jupiter ariel";
UIFont *font = [UIFont fontWithName:#"Helvetica-BoldOblique" size:28];
NSDictionary *attributes = #{NSFontAttributeName: font};
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string attributes:attributes];
self.topLabel.attributedText = attrString;
self.bottomLabel.attributedText = attrString;
}
Here's the implementation of the NonClippingLabel subclass:
#import <UIKit/UIKit.h>
#interface NonClippingLabel : UILabel
#end
#implementation NonClippingLabel
#define GUTTER 4.0f // make this large enough to accommodate the largest font in your app
- (void)drawRect:(CGRect)rect
{
// fixes word wrapping issue
CGRect newRect = rect;
newRect.origin.x = rect.origin.x + GUTTER;
newRect.size.width = rect.size.width - 2 * GUTTER;
[self.attributedText drawInRect:newRect];
}
- (UIEdgeInsets)alignmentRectInsets
{
return UIEdgeInsetsMake(0, GUTTER, 0, GUTTER);
}
- (CGSize)intrinsicContentSize
{
CGSize size = [super intrinsicContentSize];
size.width += 2 * GUTTER;
return size;
}
#end
No editing a font file, no using Core Text; just a relatively simple UILabel subclass for those using iOS 6+ and Auto Layout.
Update:
Augie caught the fact that my original solution prevented word wrapping for multi-lined text. I fixed that issue by using drawInRect: instead of drawAtPoint: to draw the text in the label's drawRect: method.
Here's a screenshot:
The top label is a plain-vanilla UILabel. The bottom label is a NonClippingLabel with an extreme gutter setting to accommodate Zapfino at size 22.0. Both labels are left and right aligned using Auto Layout.
Swift version of NonClippingLabel with fixed sizeThatFits method from bilobatum answer.
class NonClippingLabel: UILabel {
let gutter: CGFloat = 4
override func draw(_ rect: CGRect) {
super.drawText(in: rect.insetBy(dx: gutter, dy: 0))
}
override var alignmentRectInsets: UIEdgeInsets {
return .init(top: 0, left: gutter, bottom: 0, right: gutter)
}
override var intrinsicContentSize: CGSize {
var size = super.intrinsicContentSize
size.width += gutter * 2
return size
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
let fixedSize = CGSize(width: size.width - 2 * gutter, height: size.height)
let sizeWithoutGutter = super.sizeThatFits(fixedSize)
return CGSize(width: sizeWithoutGutter.width + 2 * gutter,
height: sizeWithoutGutter.height)
}
}
Rather than having to go thru a bunch of gymnastics to work around this silly Apple bug (which I did), a quick-and-dirty hack is just add a space to the end of your string to stop the last italic letter being clipped. Obviously doesn't help with multi-line labels alas, or clipped first letter descender...
Swift and SwiftUI version based on Vadim Akhmerov and bilobatum's answers. All four edges are now customizable and can be changed/updated.
Also hosted as a Github Gist: https://gist.github.com/ryanlintott/2340f35977bf2d1f7b6ea40aa379bcc6
import SwiftUI
import UIKit
struct NoClipText: UIViewRepresentable {
typealias UIViewType = NoClipLabel
let text: String
let font: UIFont
let clipExtension: EdgeSizes
func makeUIView(context: Context) -> UIViewType {
let uiView = UIViewType()
uiView.text = text
uiView.font = font
uiView.clipExtension = clipExtension
return uiView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
uiView.text = text
uiView.font = font
uiView.clipExtension = clipExtension
}
}
class NoClipLabel: UILabel {
static let defaultClipExtension: EdgeSizes = .all(10)
var clipExtension: EdgeSizes
var top: CGFloat { clipExtension.top }
var left: CGFloat { clipExtension.left }
var bottom: CGFloat { clipExtension.bottom }
var right: CGFloat { clipExtension.right }
var width: CGFloat { left + right }
var height: CGFloat { bottom + top }
required init(clipExtension: EdgeSizes = defaultClipExtension) {
self.clipExtension = clipExtension
super.init(frame: CGRect.zero)
}
override init(frame: CGRect) {
clipExtension = Self.defaultClipExtension
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
clipExtension = Self.defaultClipExtension
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
super.drawText(in: rect.inset(by: UIEdgeInsets(top: top, left: left, bottom: bottom, right: right)))
}
override var alignmentRectInsets: UIEdgeInsets {
return .init(top: top, left: left, bottom: bottom, right: right)
}
override var intrinsicContentSize: CGSize {
var size = super.intrinsicContentSize
size.width += width
size.height += height
return size
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
let fixedSize = CGSize(width: size.width - width, height: size.height - height)
let sizeWithoutExtension = super.sizeThatFits(fixedSize)
return CGSize(width: sizeWithoutExtension.width + width,
height: sizeWithoutExtension.height + height)
}
}
struct EdgeSizes: Equatable {
let top: CGFloat
let left: CGFloat
let bottom: CGFloat
let right: CGFloat
init(top: CGFloat = 0, left: CGFloat = 0, bottom: CGFloat = 0, right: CGFloat = 0) {
self.top = top
self.left = left
self.bottom = bottom
self.right = right
}
init(vertical: CGFloat = 0, horizontal: CGFloat = 0) {
self.top = vertical
self.left = horizontal
self.bottom = vertical
self.right = horizontal
}
init(_ all: CGFloat) {
self.top = all
self.left = all
self.bottom = all
self.right = all
}
static let zero = EdgeSizes(0)
static func all(_ size: CGFloat) -> EdgeSizes {
EdgeSizes(size)
}
static func vertical(_ size: CGFloat) -> EdgeSizes {
EdgeSizes(vertical: size)
}
static func horizontal(_ size: CGFloat) -> EdgeSizes {
EdgeSizes(horizontal: size)
}
}