UILabel clipping italic (oblique) text at left and right edges of content ( iOS 6+) - ios

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

Related

UILabel subclass - text cut off in bottom despite label being correct height

I have a problem with UILabel subclass cutting off text in the bottom. Label is of proper height to fit the text, there is some space left in the bottom, but the text is still being cut off.
The red stripes are border added to label's layer.
I subclass the label to add edge insets.
override func sizeThatFits(size: CGSize) -> CGSize {
var size = super.sizeThatFits(size)
size.width += insets.left + insets.right
size.height += insets.top + insets.bottom
return size
}
override func drawTextInRect(rect: CGRect) {
super.drawTextInRect(UIEdgeInsetsInsetRect(rect, insets))
}
However, in this particular case the insets are zero.
Turns out the problem was with
self.lineBreakMode = .ByClipping
changing it to
self.lineBreakMode = .ByCharWrapping
Solved the problem
I was facing the same issue with Helvetica Neue Condensed Bold font. Changing label's Baseline property from Align Baselines to Align Centers did the trick for me. You can change this easily in storyboard by selecting your label.
My problem was that the label's (vertical) content compression resistance priority was not high enough; setting it to required (1000) fixed it.
It looks like the other non-OP answers may be some sort of workaround for this same underlying issue.
TL'DR
Probably the property you are looking for is UILabel's baselineAdjustment.
It is needed because of an old UILabel's known bug. Try it:
label.baselineAdjustment = .none
Also it could be changed through interface builder. This property could be found under UILabel's Attributes inspector with the name "Baseline".
Explanation
It's a bug
There is some discussions like this one about a bug on UILabel's text bounding box. What we observe here in our case is some version of this bug. It looks like the bounding box grows in height when we shrink the text through AutoShrink .minimumFontScale or .minimumFontSize.
As a consequence, the bounding box grows bigger than the line height and the visible portion of UILabel's height. That said, with baselineAdjustment property set to it's default state, .alignBaselines, text aligns to the cropped bottom and we could observe line clipping.
Understanding this behaviour is crucial to explain why set .alignCenters solve some problems but not others. Just center text on the bigger bounding box could still clip it.
Solution
So the best approach is to set
label.baselineAdjustment = .none
The documentation for the .none case said:
Adjust text relative to the top-left corner of the bounding box. This
is the default adjustment.
Since bonding box origin matches the label's frame, it should fix any problem for a one-lined label with AutoShrink enabled.
Also it could be changed through interface builder. This property could be found under UILabel's Attributes inspector with the name "Baseline".
Documentation
You could read more here about UILabel's baselineAdjustmenton official documentation.
Happened for me when providing topAnchor and centerYAnchor for label at the same time.
Leaving just one anchor fixed the problem.
Other answers didn't help me, but what did was constraining the height of the label to whatever height it needed, like so:
let unconstrainedSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
label.heightAnchor.constraint(equalToConstant: label.sizeThatFits(unconstrainedSize).height).isActive = true
Also, sizeThatFits(_:) will return a 0 by 0 size if your label's text field is nil or equal to ""
I ran into this too, but wanted to avoid adding a height constraint. I'd already created a UILabel subclass that allowed me to add content insets (but for the purpose of setting tableHeaderView straight to a label without having to contain it in another view). Using the class I could set the bottom inset to solve the issue with the font clipping.
import UIKit
#IBDesignable class InsetLabel: UILabel {
#IBInspectable var topInset: CGFloat = 16
#IBInspectable var bottomInset: CGFloat = 16
#IBInspectable var leftInset: CGFloat = 16
#IBInspectable var rightInset: CGFloat = 16
var insets: UIEdgeInsets {
get {
return UIEdgeInsets(
top: topInset,
left: leftInset,
bottom: bottomInset,
right: rightInset
)
}
}
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: insets))
}
override var intrinsicContentSize: CGSize {
return addInsetsTo(size: super.intrinsicContentSize)
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
return addInsetsTo(size: super.sizeThatFits(size))
}
func addInsetsTo(size: CGSize) -> CGSize {
return CGSize(
width: size.width + leftInset + rightInset,
height: size.height + topInset + bottomInset
)
}
}
This could be simplified just for the font clipping to:
import UIKit
class FontFittingLabel: UILabel {
var inset: CGFloat = 16 // Adjust this
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: UIEdgeInsets(
top: 0,
left: 0,
bottom: inset,
right: 0
)))
}
override var intrinsicContentSize: CGSize {
let size = super.intrinsicContentSize
return CGSize(
width: size.width,
height: size.height + inset
)
}
}
I had a vertical UIStackView with a UILabel at the bottom. This UILabel was cutting off the letters that go below the baseline (q, g, y, etc), but only when nested inside a horizontal UIStackView. The fix was to add the .lastBaseline alignment modifier to the outer stack view.
lazy var stackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [
aVerticalStackWithUILabelAtBottom, // <-- bottom UILabel was cutoff
UIView(),
someOtherView
])
stackView.axis = .horizontal
stackView.spacing = Spacing.one
stackView.alignment = .lastBaseline // <-- BOOM fixed it
stackView.isUserInteractionEnabled = true
return stackView
}()

How can I increase the Tap Area for UIButton?

I use UIButton with auto layout. When images are small the tap area is also small. I could imagine several approaches to fix this:
increase the image size, i.e., place a transparent area around the image. This is not good because when you position the image you have to keep the extra transparent border in mind.
use CGRectInset and increase the size. This does not work well with auto layout because using auto layout it will fall back to the original image size.
Beside the two approaches above is there a better solution to increase the tap area of a UIButton?
You can simply adjust the content inset of the button to get your desired size. In code, it will look like this:
button.contentEdgeInsets = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
//Or if you specifically want to adjust around the image, instead use button.imageEdgeInsets
In interface builder, it will look like this:
Very easy. Create a custom UIButton class. Then override pointInside... method and change the value as you want.
#import "CustomButton.h"
#implementation CustomButton
-(BOOL) pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
CGRect newArea = CGRectMake(self.bounds.origin.x - 10, self.bounds.origin.y - 10, self.bounds.size.width + 20, self.bounds.size.height + 20);
return CGRectContainsPoint(newArea, point);
}
#end
It will take more 10 points touch area for every side.
And Swift 5 version:
class CustomButton: UIButton {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return bounds.insetBy(dx: -10, dy: -10).contains(point)
}
}
I confirm that Syed's solution works well even with autolayout. Here's the Swift 4.x version:
import UIKit
class BeepSmallButton: UIButton {
// MARK: - Functions
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let newArea = CGRect(
x: self.bounds.origin.x - 5.0,
y: self.bounds.origin.y - 5.0,
width: self.bounds.size.width + 10.0,
height: self.bounds.size.height + 20.0
)
return newArea.contains(point)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
You can set the button EdgeInsets in storyboard or via code. The size of button should be bigger in height and width than image set to button.
Note: After Xcode8, setting content inset is available in size inspecor
Or you can also use image view with tap gesture on it for action while taping on image view. Make sure to tick User Interaction Enabled for imageview on storyboard for gesture to work.
Make image view bigger than image to set on it and set image on it.
Now set the mode of image view image to center on storyboard/interface builder.
You can tap on image to do action.
Hope it will be helpful.
This should work
import UIKit
#IBDesignable
class GRCustomButton: UIButton {
#IBInspectable var margin:CGFloat = 20.0
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
//increase touch area for control in all directions by 20
let area = self.bounds.insetBy(dx: -margin, dy: -margin)
return area.contains(point)
}
}
Swift 5 version based on Syed's answer (negative values for a larger area):
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return bounds.insetBy(dx: -10, dy: -10).contains(point)
}
Alternatively:
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return bounds.inset(by: UIEdgeInsets(top: -5, left: -5, bottom: -5, right: -5)).contains(point)
}
Some context about the edge insets answer.
When using auto layout combined with content edge insets you may need to change your constraints.
Say you have a 10x10 image and you want to make it 30x30 for a larger hit area:
Set your auto layout constraints to the desired larger area. If you
build right now this would stretch the image.
Using the content edge insets to shrink the space available to the
image so it matches the correct size. In this Example that would 10
10 10 10. Leaving the image with a 10x10 space to draw itself in.
Win.
Both solutions presented here do work ... under the right circumstances it is. But here are some gotchas you might run into.
First something not completely obvious:
tapping has to be WITHIN the button, touching the button bounds slightly does NOT work. If a button is very small, there is a good chance most of your finger will be outside of the button and the tap won't work.
Specific to the solutions above:
SOLUTION 1 #Travis:
Use contentEdgeInsets to increase the button size without increasing the icon/text size, similar to adding padding
button.contentEdgeInsets = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
This one is straight forward, increasing the button size increases the tap area.
if you have set a height/width frame or constraint, obviously this doesn't do much, and will just distort or shift your icon/text around.
the button size will be bigger. This has to be considered when laying out other views. (offset other views as necessary)
SOLUTION 2 #Syed Sadrul Ullah Sahad:
Subclass UIButton and override point(inside point: CGPoint, with event: UIEvent?) -> Bool
class BigAreaButton: UIButton {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return bounds.insetBy(dx: -20, dy: -20).contains(point)
}
}
This solution is great because it will allow you extend the tap area beyond the views bounds without changing the layout, but here are the catches:
a parent view needs to have a background, putting a button into an otherwise empty ViewController without a background won't work.
if the button is NESTED, all views up the view hierarchy need to either provide enough "space" or override point-in as well.
e.g.
---------
| |
|oooo |
|oXXo |
|oXXo |
|oooo | Button-X nested in View-o will NOT extend beyond View-o
---------
The way I'd approach this is to give the button some extra room around a small image using contentEdgeInsets (which act like a margin outside the button content), but also override the alignmentRect property with the same insets, which bring the rect that autolayout uses back in to the image. This ensures that autolayout calculates its constraints using the smaller image, rather than the full tappable extent of the button.
class HIGTargetButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setImage(_ image: UIImage?, for state: UIControl.State) {
super.setImage(image, for: state)
guard let image = image else { return }
let verticalMarginToAdd = max(0, (targetSize.height - image.size.height) / 2)
let horizontalMarginToAdd = max(0, (targetSize.width - image.size.width) / 2)
let insets = UIEdgeInsets(top: verticalMarginToAdd,
left: horizontalMarginToAdd,
bottom: verticalMarginToAdd,
right: horizontalMarginToAdd)
contentEdgeInsets = insets
}
override var alignmentRectInsets: UIEdgeInsets {
contentEdgeInsets
}
private let targetSize = CGSize(width: 44.0, height: 44.0)
}
The pink button has a bigger tappable target (shown pink here, but could be .clear) and a smaller image - its leading edge is aligned with the green view's leading edge based on the icon, not the whole button.
Subclass UIButton and add this function
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let verticalInset = CGFloat(10)
let horizontalInset = CGFloat(10)
let largerArea = CGRect(
x: self.bounds.origin.x - horizontalInset,
y: self.bounds.origin.y - verticalInset,
width: self.bounds.size.width + horizontalInset*2,
height: self.bounds.size.height + verticalInset*2
)
return largerArea.contains(point)
}
Swift 4 • Xcode 9
You can select programmatically as -
For Image -
button.imageEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
For Title -
button.titleEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
An alternative to subclassing would be extending UIControl, adding a touchAreaInsets property to it - by leveraging the objC runtime - and swizzling pointInside:withEvent.
#import <objc/runtime.h>
#import <UIKit/UIKit.h>
#import "NSObject+Swizzling.h" // This is where the magic happens :)
#implementation UIControl (Extensions)
#dynamic touchAreaInsets;
static void * CHFLExtendedTouchAreaControlKey;
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleSelector:#selector(pointInside:withEvent:) withSelector:#selector(chfl_pointInside:event:) classMethod:NO];
});
}
- (BOOL)chfl_pointInside:(CGPoint)point event:(UIEvent *)event
{
if(UIEdgeInsetsEqualToEdgeInsets(self.touchAreaInsets, UIEdgeInsetsZero)) {
return [self chfl_pointInside:point event:event];
}
CGRect relativeFrame = self.bounds;
CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.touchAreaInsets);
return CGRectContainsPoint(hitFrame, point);
}
- (UIEdgeInsets)touchAreaInsets
{
NSValue *value = objc_getAssociatedObject(self, &CHFLExtendedTouchAreaControlKey);
if (value) {
UIEdgeInsets touchAreaInsets; [value getValue:&touchAreaInsets]; return touchAreaInsets;
}
else {
return UIEdgeInsetsZero;
}
}
- (void)setTouchAreaInsets:(UIEdgeInsets)touchAreaInsets
{
NSValue *value = [NSValue value:&touchAreaInsets withObjCType:#encode(UIEdgeInsets)];
objc_setAssociatedObject(self, &CHFLExtendedTouchAreaControlKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
#end
Here is NSObject+Swizzling.h
https://gist.github.com/epacces/fb9b8e996115b3bfa735707810f41ec8
Here is a quite generic interface that allows you to reduce/increase the touch area of UIControls.
#import <UIKit/UIKit.h>
/**
* Extends or reduce the touch area of any UIControls
*
* Example (extends the button's touch area by 20 pt):
*
* UIButton *button = [[UIButton alloc] initWithFrame:CGRectFrame(0, 0, 20, 20)]
* button.touchAreaInsets = UIEdgeInsetsMake(-10.0f, -10.0f, -10.0f, -10.0f);
*/
#interface UIControl (Extensions)
#property (nonatomic, assign) UIEdgeInsets touchAreaInsets;
#end
If you're using Material's iOS library for your buttons, you can just use hitAreaInsets to increase the touch target size of the button.
example code from https://material.io/components/buttons/ios#using-buttons
let buttonVerticalInset =
min(0, -(kMinimumAccessibleButtonSize.height - button.bounds.height) / 2);
let buttonHorizontalInset =
min(0, -(kMinimumAccessibleButtonSize.width - button.bounds.width) / 2);
button.hitAreaInsets =
UIEdgeInsetsMake(buttonVerticalInset, buttonHorizontalInset,
buttonVerticalInset, buttonHorizontalInset);
Swift 5:
UIButton subclass implementation (for programmatically created buttons).
Tap area rect can be specified as either:
Absolute rect
Edge insets (e.g. 'top:left:bottom:right')
Note: changeTapAreaBy() is applied to button's initial bounds,
unless there are previous tap area adjustments, otherwise, to those.
Usage:
let image = UIImage(systemName: "figure.surfing")
let button = UIButton.systemButton(with: image, target: nil, action: nil)
button.changeTapAreaBy(insets: UIEdgeInsets(top: -5, left: -5, bottom: 5, right: 5)
Implementation (Swift 5):
import UIKit
class ConfigurableTapAreaButton : UIButton {
var tapRect = CGRect.zero
override init(frame: CGRect) {
super.init(frame: frame)
tapRect = bounds
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return tapRect.contains(point)
}
func setTapArea(rect: CGRect) {
tapRect = rect
}
func changeTapAreaBy(insets: UIEdgeInsets) {
let dx = insets.left
let dy = insets.top
let dw = insets.right - dx
let dh = insets.bottom - dy
tapRect = CGRect( x: tapRect.origin.x + dx,
y: tapRect.origin.y + dy,
width: tapRect.size.width + dw,
height: tapRect.size.height + dh)
}
}

Vertically align text within a UILabel (Note : Using AutoLayout)

I am Copying the same Question asked Before Question.
I have tried the solutions given and was not able to solve it since sizetofit was not effective when I use Autolayout.
The expected display is like below.
Edit
In my original answer I was using the paragraph style of the label. Turns out that for multi-line labels this actually prevents the label from being multi-line. As a result I removed it from the calculation. See more about this in Github
For those of you more comfortable with using Open Source definitely look at TTTAttributedLabel where you can set the label's text alignment to TTTAttributedLabelVerticalAlignmentTop
The trick is to subclass UILabel and override drawTextInRect. Then enforce that the text is drawn at the origin of the label's bounds.
Here's a naive implementation that you can use right now:
Swift
#IBDesignable class TopAlignedLabel: UILabel {
override func drawTextInRect(rect: CGRect) {
if let stringText = text {
let stringTextAsNSString = stringText as NSString
var labelStringSize = stringTextAsNSString.boundingRectWithSize(CGSizeMake(CGRectGetWidth(self.frame), CGFloat.max),
options: NSStringDrawingOptions.UsesLineFragmentOrigin,
attributes: [NSFontAttributeName: font],
context: nil).size
super.drawTextInRect(CGRectMake(0, 0, CGRectGetWidth(self.frame), ceil(labelStringSize.height)))
} else {
super.drawTextInRect(rect)
}
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
layer.borderWidth = 1
layer.borderColor = UIColor.blackColor().CGColor
}
}
Swift 3
#IBDesignable class TopAlignedLabel: UILabel {
override func drawText(in rect: CGRect) {
if let stringText = text {
let stringTextAsNSString = stringText as NSString
let labelStringSize = stringTextAsNSString.boundingRect(with: CGSize(width: self.frame.width,height: CGFloat.greatestFiniteMagnitude),
options: NSStringDrawingOptions.usesLineFragmentOrigin,
attributes: [NSFontAttributeName: font],
context: nil).size
super.drawText(in: CGRect(x:0,y: 0,width: self.frame.width, height:ceil(labelStringSize.height)))
} else {
super.drawText(in: rect)
}
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
layer.borderWidth = 1
layer.borderColor = UIColor.black.cgColor
}
}
Objective-C
IB_DESIGNABLE
#interface TopAlignedLabel : UILabel
#end
#implementation TopAlignedLabel
- (void)drawTextInRect:(CGRect)rect {
if (self.text) {
CGSize labelStringSize = [self.text boundingRectWithSize:CGSizeMake(CGRectGetWidth(self.frame), CGFLOAT_MAX)
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
attributes:#{NSFontAttributeName:self.font}
context:nil].size;
[super drawTextInRect:CGRectMake(0, 0, ceilf(CGRectGetWidth(self.frame)),ceilf(labelStringSize.height))];
} else {
[super drawTextInRect:rect];
}
}
- (void)prepareForInterfaceBuilder {
[super prepareForInterfaceBuilder];
self.layer.borderWidth = 1;
self.layer.borderColor = [UIColor blackColor].CGColor;
}
#end
Since I used IBDesignable you can add this label to a storyboard and watch it go, this is what it looks like for me
If you're not restricted by having UILabel of fixed size, instead of aligning the text within a UILabel, simply use ≥ constraint on the given label to change the size of it.
It's the most elegant solution using Auto Layout. Don't forget to set numberOfLines to zero though.
You can use UITextView instead of UILabel:
Uncheck "Scrolling enabled"
Uncheck "Editable"
Uncheck "Selectable"
Set background color to ClearColor
I had the same problem, and this is how I solved it. I just edited the Baseline under Attribute Inspector for the Label. Set it to "Align Centers".
Instead, I changed the Bottom Space Constant to priority #250 and solved my problem. And my label has height constant with <= constant
You would do that by removing the minimum height.
If you need the minimum height to something else below the label then you would use a container view that resized based on the label contents but used a minimum.
Auto layout only work with edges/sizes of controller, not with controllers content.so its not a good idea to use auto layout to display your label text on top of first line.
according to me sizetofit is a best option to do so.
I used #Daniel Golasko's solution and whenever the text inside the UILabel was longer than the UILabel could contain, the text would start moving down instead of staying aligned to top.
I changed this line to make sure the text is aligned properly
[super drawTextInRect:CGRectMake(0, 0, ceilf(CGRectGetWidth(self.frame)),MIN(ceilf(labelStringSize.height), self.frame.size.height))];
I had a similiar issue where there were 3 labels. The middle label could have much longer text than the other two, so its height could grow much larger.
Like this:
I set the middle label's bottom space constraint to be >= the bottom label.
That solved my problem.
Here's an improvement on the Swift 3 solution by Daniel Galasko (here you can also set the maximum line number without an offset on the top):
import UIKit
#IBDesignable class TopAlignedLabel: UILabel {
override func drawText(in rect: CGRect) {
if let stringText = text {
let stringTextAsNSString = stringText as NSString
let labelString = stringTextAsNSString.boundingRect(with: CGSize(width: frame.width, height: .greatestFiniteMagnitude),
options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
super.drawText(in: CGRect(x: 0, y: 0, width: frame.width, height: ceil(labelString.size.height) > frame.height ? frame.height : ceil(labelString.size.height)))
} else {
super.drawText(in: rect)
}
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
layer.borderWidth = 1
layer.borderColor = UIColor.black.cgColor
}
}
There is an easy solution for cases where the height of a label doesn't need to be constant: put your Label in a Stack View. Be sure to add leading and trailing constants to the Stack View. Here is a screenshot of how to do it in storyboard:
Swift 4
You should subclass UILabel and override text display rendering.
class UITopAlignedLabel: UILabel {
override func drawText(in rect: CGRect) {
guard let string = text else {
super.drawText(in: rect)
return
}
let size = (string as NSString).boundingRect(
with: CGSize(width: rect.width, height: .greatestFiniteMagnitude),
options: [.usesLineFragmentOrigin],
attributes: [.font: font],
context: nil).size
var rect = rect
rect.size.height = size.height.rounded()
super.drawText(in: rect)
}
}
You can try if button [button setContentVerticalAlignment:UIControlContentVerticalAlignmentTop];
Edit :
You can try with this if you want to use label only:
https://stackoverflow.com/a/11278660/1223897
For me, I didn't set the height constraint, the text always grows from the top of the label.
The constraints for this label are top, left, right.
By the way, my label has fixed line numbers, so no worries about the height.
#IBInspectable var alignTop: Bool = false
func setAlignTop() {
let text = self.text!
let lines = text.characters.split(separator: "\n").count
if lines < self.numberOfLines {
var newLines = ""
for _ in 0..<(self.numberOfLines - lines) {
newLines = newLines.appending("\n ")
}
self.text! = text.appending(newLines)
}
}
override var text: String? {
didSet {
if alignTop {
self.setAlignTop()
}
}
}
use this my class, you can change text alignment by contentMode.
supported case: .top, .bottom, .left, .right, .topLeft, .topRight, .bottomLeft, .bottomRight
Swift4
import Foundation
import UIKit
#IBDesignable
class UIAlignedLabel: UILabel {
override func drawText(in rect: CGRect) {
if let text = text as NSString? {
func defaultRect(for maxSize: CGSize) -> CGRect {
let size = text
.boundingRect(
with: maxSize,
options: NSStringDrawingOptions.usesLineFragmentOrigin,
attributes: [
NSAttributedStringKey.font: font
],
context: nil
).size
let rect = CGRect(
origin: .zero,
size: CGSize(
width: min(frame.width, ceil(size.width)),
height: min(frame.height, ceil(size.height))
)
)
return rect
}
switch contentMode {
case .top, .bottom, .left, .right, .topLeft, .topRight, .bottomLeft, .bottomRight:
let maxSize = CGSize(width: frame.width, height: frame.height)
var rect = defaultRect(for: maxSize)
switch contentMode {
case .bottom, .bottomLeft, .bottomRight:
rect.origin.y = frame.height - rect.height
default: break
}
switch contentMode {
case .right, .topRight, .bottomRight:
rect.origin.x = frame.width - rect.width
default: break
}
super.drawText(in: rect)
default:
super.drawText(in: rect)
}
} else {
super.drawText(in: rect)
}
}
}
In the Interface Builder, just make the height <= some value instead of =. This will enable to text to start at the top and expand the height as needed. For example, I have a label with a height proportional to the size of the main view. So my height constraint looks like this:
Height Constraint

Resizing a UILabel to accommodate insets

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
}

Add lefthand margin to UITextField

I want to put the left margin of a UITextField's text at 10 px. What is the best way to do that?
You can do it by extending UITextField class and overriding two methods:
- (CGRect)textRectForBounds:(CGRect)bounds;
- (CGRect)editingRectForBounds:(CGRect)bounds;
Here is the code:
The interface in MYTextField.h
#interface MYTextField : UITextField
#end
Its implementation in MYTextField.m
#implementation MYTextField
static CGFloat leftMargin = 28;
- (CGRect)textRectForBounds:(CGRect)bounds
{
bounds.origin.x += leftMargin;
return bounds;
}
- (CGRect)editingRectForBounds:(CGRect)bounds
{
bounds.origin.x += leftMargin;
return bounds;
}
#end
As I have explained in a previous comment, the best solution in this case is to extend the UITextField class instead of using a category, so you can use it explicitly on the desired text fields.
#import <UIKit/UIKit.h>
#interface MYTextField : UITextField
#end
#implementation MYTextField
- (CGRect)textRectForBounds:(CGRect)bounds {
int margin = 10;
CGRect inset = CGRectMake(bounds.origin.x + margin, bounds.origin.y, bounds.size.width - margin, bounds.size.height);
return inset;
}
- (CGRect)editingRectForBounds:(CGRect)bounds {
int margin = 10;
CGRect inset = CGRectMake(bounds.origin.x + margin, bounds.origin.y, bounds.size.width - margin, bounds.size.height);
return inset;
}
#end
A category is intended to add new functions to an existing class, not to override an existing method.
UITextField * textField = [[UITextField alloc]init];
[textField setDelegate:self];
[textField setFrame:CGRectMake(170,112,140,25)];
[textField setBorderStyle:UITextBorderStyleNone];
[textField setBackgroundColor:[UIColor clearColor]];
[self.View addSubview:noofChildTField];
UIView *paddingView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 5, 20)] autorelease];
textField.leftView = paddingView;
textField.leftViewMode = UITextFieldViewModeAlways;
Try this code
For Swift 3 :
Make an outlet of the UITextField, say usernameTextField. Then write the following code in viewDidLoad()
let paddingView : UIView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: 20))
usernameTextField.leftView = paddingView
usernameTextField.leftViewMode = .always
Change the width: 5 to a greater value if more space is required.
I was hoping to see a setting in IB in the property inspector to adjust this value. Turns out I had set alignment and border style inadvertently to values that messed with the padding on the left.
You wouldn't think it would be a big deal to choose left justify and no border but it makes the words basically overlap or come right to the edge of the box and it doesn't look right.
Bad:
Good:
Fixed it right up for me. Hope this helps someone else as well.
Subclass the UITextField and add an extension:
extension UITextField {
func paddingLeft(inset: CGFloat){
self.leftView = UIView(frame: CGRect(x: 0, y: 0, width: inset, height: self.frame.height))
self.leftViewMode = UITextField.ViewMode.always
}
}
usage
class MyUITextFieldClass: UITextField {
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
self.paddingLeft(inset: 10)
}
}
i have reached almost by overriding - (CGRect)textRectForBounds:(CGRect)bounds. now issue is when TextField goes in to edit mode their left margin reset to Zero .......
#implementation UITextField(UITextFieldCatagory)
- (CGRect)textRectForBounds:(CGRect)bounds {
CGRect theRect=CGRectMake(bounds.origin.x+10, bounds.origin.y, bounds.size.width-10, bounds.size.height);
return theRect;
}
For Swift 4:
I prefer to use IBDesignable class and IBInspectable properties to allow me to set the padding via Xcode storyboards and keep it reusable. I've also updated the code to work in Swift 4.
import Foundation
import UIKit
#IBDesignable
class PaddableTextField: UITextField {
var padding = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
#IBInspectable var left: CGFloat = 0 {
didSet {
adjustPadding()
}
}
#IBInspectable var right: CGFloat = 0 {
didSet {
adjustPadding()
}
}
#IBInspectable var top: CGFloat = 0 {
didSet {
adjustPadding()
}
}
#IBInspectable var bottom: CGFloat = 0 {
didSet {
adjustPadding()
}
}
func adjustPadding() {
padding = UIEdgeInsets(top: top, left: left, bottom: bottom, right: right)
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
}
override func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.inset(by: UIEdgeInsets(top: top, left: left, bottom: bottom, right: right))
}
override func placeholderRect(forBounds bounds: CGRect) -> CGRect {
return bounds.inset(by: UIEdgeInsets(top: top, left: left, bottom: bottom, right: right))
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return bounds.inset(by: UIEdgeInsets(top: top, left: left, bottom: bottom, right: right))
}
}
You can try this
TextField.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;
TextField.textAlignment = UITextAlignmentCenter;

Resources