How can I increase the Tap Area for UIButton?

IosObjective CSwiftUibutton

Ios Problem Overview


I use UIButton with auto layout. When images are small the tap area is also small. I could imagine several approaches to fix this:

  1. 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.
  2. 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?

Ios Solutions


Solution 1 - Ios

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:

interface builder

Solution 2 - Ios

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

Solution 3 - Ios

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

Solution 4 - Ios

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 UIEdgeInseton UIButton

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.

Using image view with tap action and image set on it as center mode You can tap on image to do action.

Hope it will be helpful.

Solution 5 - Ios

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

}

Solution 6 - Ios

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

Solution 7 - Ios

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:

  1. Set your auto layout constraints to the desired larger area. If you build right now this would stretch the image.

  2. 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.

  3. Win.

Solution 8 - Ios

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)

Solution 9 - Ios

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

Solution 10 - Ios

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.

Pink tappable button aligned with green view based on a smaller icon

Solution 11 - Ios

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

Solution 12 - Ios

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

Solution 13 - Ios

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionSven BauerView Question on Stackoverflow
Solution 1 - IosTravisView Answer on Stackoverflow
Solution 2 - IosSyed Sadrul Ullah SahadView Answer on Stackoverflow
Solution 3 - IosGlenn PosadasView Answer on Stackoverflow
Solution 4 - Iosuser2094867View Answer on Stackoverflow
Solution 5 - IosAlexView Answer on Stackoverflow
Solution 6 - IosMatty OView Answer on Stackoverflow
Solution 7 - IosKevin KruusiView Answer on Stackoverflow
Solution 8 - IosJackView Answer on Stackoverflow
Solution 9 - IosLucas ChweView Answer on Stackoverflow
Solution 10 - IosZoë SmithView Answer on Stackoverflow
Solution 11 - IosSu-Au HwangView Answer on Stackoverflow
Solution 12 - IosHepaKKesView Answer on Stackoverflow
Solution 13 - IosQianren ZhouView Answer on Stackoverflow