How can I increase the Tap Area for UIButton? - ios

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

Related

IntrinsicContentSize on a custom UIView streches the content

I would like to create a custom UIView which uses/offers a IntrinsicContentSize, since its height depends on its content (just like a label where the height depends on the text).
While I found a lot of information on how to work with IntrinsicContentSize offered by existing Views, I found just a few bits on how to use IntrinsicContentSize on a custom UIView:
#IBDesignable class MyIntrinsicView: UIView {
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
context?.setFillColor(UIColor.gray.cgColor)
context?.fill(CGRect(x: 0, y: 0, width: frame.width, height: 25))
height = 300
invalidateIntrinsicContentSize()
}
#IBInspectable var height: CGFloat = 50
override var intrinsicContentSize: CGSize {
return CGSize(width: super.intrinsicContentSize.width, height: height)
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
invalidateIntrinsicContentSize()
}
}
The initial high is set to 50
The draw methods draws a gray rect with a size 25
height is changed to 300 and invalidateIntrinsicContentSize() is called
Placing a MyView instance in InterfaceBuilder works without any problem. The view does not need a height constraint
However, in IB the initial height of 50 is used. Why? IB draws the gray rect, thus the draw method is called. So why is the height not changed to 300?
What is also strange: When setting a background color it is drawn as well, also super.draw(...) is not called. Is this intended?
I would expect a view with height of 300 and a gray rect at the top with a height of 25. However, when running the project in simulator the result is different:
height of the view = 300 - OK
height of content (= gray rect) = 150 (half view height) - NOT OK
It seems that the content was stretched from its original height of 25 to keep its relative height to the view. Why is this?
Trying to change the view's height from inside draw() is probably a really bad idea.
First, as you've seen, changing the intrinsic content size does not trigger a redraw. Second, if it did, your code would go into an infinite recursion loop.
Take a look at this edit to your class:
#IBDesignable class MyIntrinsicView: UIView {
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
context?.setFillColor(UIColor.gray.cgColor)
context?.fill(CGRect(x: 0, y: 0, width: frame.width, height: 25))
// probably a really bad idea to do this inside draw()
//height = 300
//invalidateIntrinsicContentSize()
}
#IBInspectable var height: CGFloat = 50 {
didSet {
// call when height var is set
invalidateIntrinsicContentSize()
// we need to trigger draw()
setNeedsDisplay()
}
}
override var intrinsicContentSize: CGSize {
return CGSize(width: super.intrinsicContentSize.width, height: height)
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
// not needed
//invalidateIntrinsicContentSize()
}
}
Now, when you change the intrinsic height in IB via the IBDesignable property, it will update in your Storyboard properly.
Here's a quick look at using it at run-time. Each tap (anywhere) will increase the height property by 50 (until we get over 300, when it will be reset to 50), which then invalidates the intrinsic content size and forces a call to draw():
class QuickTestVC: UIViewController {
#IBOutlet var testView: MyIntrinsicView!
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
var h: CGFloat = testView.intrinsicContentSize.height
h += 50
if h > 300 {
h = 50
}
testView.height = h
}
}

Changing UISwitch width and height

I am trying to change the default height and width of a UISwitch element in iOS, but unsuccessfully.
Can you change the default height and width of a UISwitch element?
Should the element be created programmatically?
I tested the theory and it appears that you can use a scale transform to increase the size of the UISwitch
UISwitch *aSwitch = [[UISwitch alloc] initWithFrame:CGRectMake(120, 120, 51, 31)];
aSwitch.transform = CGAffineTransformMakeScale(2.0, 2.0);
[self.view addSubview:aSwitch];
Swift 4
#IBOutlet weak var switchDemo: UISwitch!
override func viewDidLoad() {
super.viewDidLoad()
switchDemo.transform = CGAffineTransform(scaleX: 0.75, y: 0.75)
}
Swift 5:
import UIKit
extension UISwitch {
func set(width: CGFloat, height: CGFloat) {
let standardHeight: CGFloat = 31
let standardWidth: CGFloat = 51
let heightRatio = height / standardHeight
let widthRatio = width / standardWidth
transform = CGAffineTransform(scaleX: widthRatio, y: heightRatio)
}
}
Not possible. A UISwitch has a locked intrinsic height of 51 x 31 .
You can force constraints on the switch at design time in the xib...
but come runtime it will snap back to its intrinsic size.
You can supply another image via the .onImage / .offImage properties but again from the docs.
The size of this image must be less than or equal to 77 points wide
and 27 points tall. If you specify larger images, the edges may be
clipped.
You are going to have to bake your own custom one if you want another size.
here is a nice UISwitch subclass that i wrote for this purpose, its also IBDesignable so you can control it from your Storyboard / xib
#IBDesignable class BigSwitch: UISwitch {
#IBInspectable var scale : CGFloat = 1{
didSet{
setup()
}
}
//from storyboard
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
//from code
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
private func setup(){
self.transform = CGAffineTransform(scaleX: scale, y: scale)
}
override func prepareForInterfaceBuilder() {
setup()
super.prepareForInterfaceBuilder()
}
}
import UIKit
extension UISwitch {
static let standardHeight: CGFloat = 31
static let standardWidth: CGFloat = 51
#IBInspectable var width: CGFloat {
set {
set(width: newValue, height: height)
}
get {
frame.width
}
}
#IBInspectable var height: CGFloat {
set {
set(width: width, height: newValue)
}
get {
frame.height
}
}
func set(width: CGFloat, height: CGFloat) {
let heightRatio = height / UISwitch.standardHeight
let widthRatio = width / UISwitch.standardWidth
transform = CGAffineTransform(scaleX: widthRatio, y: heightRatio)
}
}
Even if it’s possible to make a UISwitch smaller, this would negatively effect the user experience. Apple's Human Interface Guidelines recommend a minimum size of 44 points for touch targets.
Provide ample touch targets for interactive elements. Try to maintain a minimum tappable area of 44pt x 44pt for all controls
By scaling this to smaller than the standard size, it will become more difficult for users to tap, and also introduce accessibility concerns. Please consider users with less than perfect vision or motor control before making UI elements small.
Finally, here’s an excerpt from a great article about touch target sizes illustrating what can happen when controls are too small.
Interviewer — “I noticed, you had some trouble submitting your email address on this screen, can you tell me how that felt?”
User — “Oh yeah, I’m not very good at technology.”
Interviewer — “What do you think was causing you to struggle at that point?”
User — “The buttons were hard to tap, and I just kept stuffing it up.”

Make UISlider height larger?

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

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
}

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

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

Resources