Reputation: 3122
I have a question dealing with UIButton and its hit area. I am using the Info Dark button in interface builder, but I am finding that the hit area is not large enough for some people's fingers.
Is there a way to increase the hit area of a button either programmatically or in Interface Builder without changing the size of the InfoButton graphic?
Upvotes: 196
Views: 103755
Reputation: 12582
class ChubbyButton: UIButton {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return bounds.insetBy(dx: -20, dy: -20).contains(point)
}
}
Note that this works perfectly for any type of UIView:
class YourEasyToTouchView: UIView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return bounds.insetBy(dx: -20, dy: -20).contains(point)
}
}
class RightwardsButton: UIButton {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return bounds.inset(by:
UIEdgeInsets(top: 0, left: 0, bottom: 0, right: -100))
.contains(point)
}
}
You can now click on the pink area as well as the button itself:
Upvotes: 4
Reputation: 5405
@antoine's answer formatted for Swift 4
final class ExtendedHitButton: UIButton {
override func point( inside point: CGPoint, with event: UIEvent? ) -> Bool {
let relativeFrame = self.bounds
let hitTestEdgeInsets = UIEdgeInsets(top: -44, left: -44, bottom: -44, right: -44) // Apple recommended hit target
let hitFrame = relativeFrame.inset(by: hitTestEdgeInsets)
return hitFrame.contains( point );
}
}
Upvotes: 0
Reputation: 8677
I'm seeing a lot of solutions that either don't quite hit the mark or require specifying some fixed insets to add. Here's a solution for a simple UIView
subclass that will extend the hit rect of the view to at least 44 x 44; if either of the dimensions is already greater than that, then it doesn't artificially pad that dimension.
This ensures that a button will always have the recommended minimum touch size of 44 x 44 without needing any manual configuration, calculation, or image padding:
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let minimumTouchSize: CGFloat = 44.0
let center: CGPoint = .init(x: self.bounds.midX, y: self.bounds.midY)
let minimumHitRect: CGRect =
.init(center: center, size: .zero)
.insetBy(
dx: -minimumTouchSize / 2.0,
dy: -minimumTouchSize / 2.0
)
let fullHitRect = self.bounds.union(minimumHitRect)
return fullHitRect.contains(point)
}
Upvotes: 1
Reputation: 251
Similar to Zhanserik's, with variable extension and updated for Swift 4.2:
class ButtonWithExtendedHitArea: UIButton {
var extention: CGFloat
required init(extendBy: CGFloat) {
extention = extendBy
super.init(frame: .zero)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let relativeFrame = self.bounds
let hitTestEdgeInsets = UIEdgeInsets(top: -extention, left: -extention, bottom: -extention, right: -extention)
let hitFrame = relativeFrame.inset(by: hitTestEdgeInsets)
return hitFrame.contains(point)
}
}
Upvotes: 0
Reputation: 11191
Since I am using a background image, none of these solutions worked well for me. Here is a solution that does some fun objective-c magic and offers a drop in solution with minimal code.
First, add a category to UIButton
that overrides the hit test and also adds a property for expanding the hit test frame.
UIButton+Extensions.h
@interface UIButton (Extensions)
@property(nonatomic, assign) UIEdgeInsets hitTestEdgeInsets;
@end
UIButton+Extensions.m
#import "UIButton+Extensions.h"
#import <objc/runtime.h>
@implementation UIButton (Extensions)
@dynamic hitTestEdgeInsets;
static const NSString *KEY_HIT_TEST_EDGE_INSETS = @"HitTestEdgeInsets";
-(void)setHitTestEdgeInsets:(UIEdgeInsets)hitTestEdgeInsets {
NSValue *value = [NSValue value:&hitTestEdgeInsets withObjCType:@encode(UIEdgeInsets)];
objc_setAssociatedObject(self, &KEY_HIT_TEST_EDGE_INSETS, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(UIEdgeInsets)hitTestEdgeInsets {
NSValue *value = objc_getAssociatedObject(self, &KEY_HIT_TEST_EDGE_INSETS);
if(value) {
UIEdgeInsets edgeInsets; [value getValue:&edgeInsets]; return edgeInsets;
}else {
return UIEdgeInsetsZero;
}
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
if(UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero) || !self.enabled || self.hidden) {
return [super pointInside:point withEvent:event];
}
CGRect relativeFrame = self.bounds;
CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.hitTestEdgeInsets);
return CGRectContainsPoint(hitFrame, point);
}
@end
Once this class is added, all you need to do is set the edge insets of your button. Note that I chose to add the insets so if you want to make the hit area larger, you must use negative numbers.
[button setHitTestEdgeInsets:UIEdgeInsetsMake(-10, -10, -10, -10)];
Note: Remember to import the category (#import "UIButton+Extensions.h"
) in your classes.
Upvotes: 138
Reputation: 2384
My solution on Swift 3:
class MyButton: UIButton {
override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let relativeFrame = self.bounds
let hitTestEdgeInsets = UIEdgeInsetsMake(-25, -25, -25, -25)
let hitFrame = UIEdgeInsetsInsetRect(relativeFrame, hitTestEdgeInsets)
return hitFrame.contains(point)
}
}
Upvotes: 14
Reputation: 2836
My take:
open class MinimumTouchAreaButton: UIButton {
open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard !self.isHidden, self.isUserInteractionEnabled, self.alpha > 0 else { return nil }
let expandedBounds = bounds.insetBy(dx: min(bounds.width - 44, 0), dy: min(bounds.height - 44, 0))
return expandedBounds.contains(point) ? self : nil
}
}
Upvotes: -2
Reputation: 57
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let inset = UIEdgeInsets(top: -adjustHitY * 0.5, left: -adjustHitX * 0.5, bottom: -adjustHitY * 0.5, right: -adjustHitX * 0.5)
return UIEdgeInsetsInsetRect(bounds, inset).contains(point)
}
Upvotes: -2
Reputation: 594
Here's Chase's UIButton+Extensions in Swift 3.0.
import UIKit
private var pTouchAreaEdgeInsets: UIEdgeInsets = .zero
extension UIButton {
var touchAreaEdgeInsets: UIEdgeInsets {
get {
if let value = objc_getAssociatedObject(self, &pTouchAreaEdgeInsets) as? NSValue {
var edgeInsets: UIEdgeInsets = .zero
value.getValue(&edgeInsets)
return edgeInsets
}
else {
return .zero
}
}
set(newValue) {
var newValueCopy = newValue
let objCType = NSValue(uiEdgeInsets: .zero).objCType
let value = NSValue(&newValueCopy, withObjCType: objCType)
objc_setAssociatedObject(self, &pTouchAreaEdgeInsets, value, .OBJC_ASSOCIATION_RETAIN)
}
}
open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if UIEdgeInsetsEqualToEdgeInsets(self.touchAreaEdgeInsets, .zero) || !self.isEnabled || self.isHidden {
return super.point(inside: point, with: event)
}
let relativeFrame = self.bounds
let hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.touchAreaEdgeInsets)
return hitFrame.contains(point)
}
}
To use it, you can:
button.touchAreaEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
Upvotes: 35
Reputation: 1118
You could also subclass UIButton
or a custom UIView
and override point(inside:with:)
with something like:
Swift 3
override func point(inside point: CGPoint, with _: UIEvent?) -> Bool {
let margin: CGFloat = 5
let area = self.bounds.insetBy(dx: -margin, dy: -margin)
return area.contains(point)
}
Objective-C
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGFloat margin = 5.0;
CGRect area = CGRectInset(self.bounds, -margin, -margin);
return CGRectContainsPoint(area, point);
}
Upvotes: 51
Reputation: 956
Here's an elegant solution using Extensions in Swift. It gives all UIButtons a hit area of at least 44x44 points, as per Apple's Human Interface Guidelines (https://developer.apple.com/ios/human-interface-guidelines/visual-design/layout/)
Swift 2:
private let minimumHitArea = CGSizeMake(44, 44)
extension UIButton {
public override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
// if the button is hidden/disabled/transparent it can't be hit
if self.hidden || !self.userInteractionEnabled || self.alpha < 0.01 { return nil }
// increase the hit frame to be at least as big as `minimumHitArea`
let buttonSize = self.bounds.size
let widthToAdd = max(minimumHitArea.width - buttonSize.width, 0)
let heightToAdd = max(minimumHitArea.height - buttonSize.height, 0)
let largerFrame = CGRectInset(self.bounds, -widthToAdd / 2, -heightToAdd / 2)
// perform hit test on larger frame
return (CGRectContainsPoint(largerFrame, point)) ? self : nil
}
}
Swift 3:
fileprivate let minimumHitArea = CGSize(width: 100, height: 100)
extension UIButton {
open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// if the button is hidden/disabled/transparent it can't be hit
if self.isHidden || !self.isUserInteractionEnabled || self.alpha < 0.01 { return nil }
// increase the hit frame to be at least as big as `minimumHitArea`
let buttonSize = self.bounds.size
let widthToAdd = max(minimumHitArea.width - buttonSize.width, 0)
let heightToAdd = max(minimumHitArea.height - buttonSize.height, 0)
let largerFrame = self.bounds.insetBy(dx: -widthToAdd / 2, dy: -heightToAdd / 2)
// perform hit test on larger frame
return (largerFrame.contains(point)) ? self : nil
}
}
Upvotes: 67
Reputation: 358
This is my Swift 3 Solution(based on this blogpost: http://bdunagan.com/2010/03/01/iphone-tip-larger-hit-area-for-uibutton/)
class ExtendedHitAreaButton: UIButton {
@IBInspectable var hitAreaExtensionSize: CGSize = CGSize(width: -10, height: -10)
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let extendedFrame: CGRect = bounds.insetBy(dx: hitAreaExtensionSize.width, dy: hitAreaExtensionSize.height)
return extendedFrame.contains(point) ? self : nil
}
}
Upvotes: 5
Reputation: 3418
Base on giaset's answer above (which I found the most elegant solution), here is the swift 3 version:
import UIKit
fileprivate let minimumHitArea = CGSize(width: 44, height: 44)
extension UIButton {
open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// if the button is hidden/disabled/transparent it can't be hit
if isHidden || !isUserInteractionEnabled || alpha < 0.01 { return nil }
// increase the hit frame to be at least as big as `minimumHitArea`
let buttonSize = bounds.size
let widthToAdd = max(minimumHitArea.width - buttonSize.width, 0)
let heightToAdd = max(minimumHitArea.height - buttonSize.height, 0)
let largerFrame = bounds.insetBy(dx: -widthToAdd / 2, dy: -heightToAdd / 2)
// perform hit test on larger frame
return (largerFrame.contains(point)) ? self : nil
}
}
Upvotes: 3
Reputation: 2102
I just did the port of the @Chase solution in swift 2.2
import Foundation
import ObjectiveC
private var hitTestEdgeInsetsKey: UIEdgeInsets = UIEdgeInsetsZero
extension UIButton {
var hitTestEdgeInsets:UIEdgeInsets {
get {
let inset = objc_getAssociatedObject(self, &hitTestEdgeInsetsKey) as? NSValue ?? NSValue(UIEdgeInsets: UIEdgeInsetsZero)
return inset.UIEdgeInsetsValue()
}
set {
let inset = NSValue(UIEdgeInsets: newValue)
objc_setAssociatedObject(self, &hitTestEdgeInsetsKey, inset, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
public override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
guard !UIEdgeInsetsEqualToEdgeInsets(hitTestEdgeInsets, UIEdgeInsetsZero) && self.enabled == true && self.hidden == false else {
return super.pointInside(point, withEvent: event)
}
let relativeFrame = self.bounds
let hitFrame = UIEdgeInsetsInsetRect(relativeFrame, hitTestEdgeInsets)
return CGRectContainsPoint(hitFrame, point)
}
}
an you can use like this
button.hitTestEdgeInsets = UIEdgeInsetsMake(-10, -10, -10, -10)
For any other reference see https://stackoverflow.com/a/13067285/1728552
Upvotes: 1
Reputation: 14296
Swift:
override func viewWillAppear(animated: Bool) {
self.sampleButton.frame = CGRectInset(self.sampleButton.frame, -10, -10);
}
Upvotes: -1
Reputation: 34503
This Swift version lets you define a minimum hit size for all UIButtons. Crucially, it also handles the case when UIButtons are hidden, which many answers neglect.
extension UIButton {
public override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
// Ignore if button hidden
if self.hidden {
return nil
}
// If here, button visible so expand hit area
let hitSize = CGFloat(56.0)
let buttonSize = self.frame.size
let widthToAdd = (hitSize - buttonSize.width > 0) ? hitSize - buttonSize.width : 0
let heightToAdd = (hitSize - buttonSize.height > 0) ? hitSize - buttonSize.height : 0
let largerFrame = CGRect(x: 0-(widthToAdd/2), y: 0-(heightToAdd/2), width: buttonSize.width+widthToAdd, height: buttonSize.height+heightToAdd)
return (CGRectContainsPoint(largerFrame, point)) ? self : nil
}
}
Upvotes: 0
Reputation: 12667
Implementation through override inherited UIButton.
Swift 2.2:
// don't forget that negative values are for outset
_button.hitOffset = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
...
class UICustomButton: UIButton {
var hitOffset = UIEdgeInsets()
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
guard hitOffset != UIEdgeInsetsZero && enabled && !hidden else {
return super.pointInside(point, withEvent: event)
}
return UIEdgeInsetsInsetRect(bounds, hitOffset).contains(point)
}
}
Upvotes: 3
Reputation: 1960
Chase's custom hit test implemented as a subclass of UIButton. Written in Objective-C.
It seems to work both for init
and buttonWithType:
constructors. For my needs it's perfect, but since subclassing UIButton
can be hairy, I'd be interested to know if anyone has a fault with it.
#import <UIKit/UIKit.h>
@interface CustomHitAreaButton : UIButton
- (void)setHitTestEdgeInsets:(UIEdgeInsets)hitTestEdgeInsets;
@end
#import "CustomHitAreaButton.h"
@interface CustomHitAreaButton()
@property (nonatomic, assign) UIEdgeInsets hitTestEdgeInsets;
@end
@implementation CustomHitAreaButton
- (instancetype)initWithFrame:(CGRect)frame {
if(self = [super initWithFrame:frame]) {
self.hitTestEdgeInsets = UIEdgeInsetsZero;
}
return self;
}
-(void)setHitTestEdgeInsets:(UIEdgeInsets)hitTestEdgeInsets {
self->_hitTestEdgeInsets = hitTestEdgeInsets;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
if(UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero) || !self.enabled || self.hidden) {
return [super pointInside:point withEvent:event];
}
CGRect relativeFrame = self.bounds;
CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.hitTestEdgeInsets);
return CGRectContainsPoint(hitFrame, point);
}
@end
Upvotes: 3
Reputation: 15579
I made a library for this very purpose.
You can choose to use a UIView
category, no subclassing required:
@interface UIView (KGHitTesting)
- (void)setMinimumHitTestWidth:(CGFloat)width height:(CGFloat)height;
@end
Or you can subclass your UIView or UIButton and set the minimumHitTestWidth
and/or minimumHitTestHeight
. Your button hit-test area will then be represented by these 2 values.
Just like other solutions, it uses the - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
method. The method is called when iOS performs hit-testing. This blog post has a good description on how iOS hit-testing works.
https://github.com/kgaidis/KGHitTestingViews
@interface KGHitTestingButton : UIButton <KGHitTesting>
@property (nonatomic) CGFloat minimumHitTestHeight;
@property (nonatomic) CGFloat minimumHitTestWidth;
@end
You can also just subclass and use the Interface Builder without writing any code:
Upvotes: 2
Reputation: 1019
None of the answers works perfect for me, because I use background image and a title on that button. Moreover, the button will resize as screen size changes.
Instead, I enlarge the tap area by making the png transparent area larger.
Upvotes: 0
Reputation: 54111
I'm using a more generic approach by swizzling -[UIView pointInside:withEvent:]
. This allows me to modify hit testing behavior on any UIView
, not just UIButton
.
Oftentimes, a button is placed inside a container view that also limits the hit testing. For instance, when a button is at the top of a container view and you want to extend the touch target upwards, you also have to extend the touch target of the container view.
@interface UIView(Additions)
@property(nonatomic) UIEdgeInsets hitTestEdgeInsets;
@end
@implementation UIView(Additions)
+ (void)load {
Swizzle(self, @selector(pointInside:withEvent:), @selector(myPointInside:withEvent:));
}
- (BOOL)myPointInside:(CGPoint)point withEvent:(UIEvent *)event {
if(UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero) || self.hidden ||
([self isKindOfClass:UIControl.class] && !((UIControl*)self).enabled))
{
return [self myPointInside:point withEvent:event]; // original implementation
}
CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets);
hitFrame.size.width = MAX(hitFrame.size.width, 0); // don't allow negative sizes
hitFrame.size.height = MAX(hitFrame.size.height, 0);
return CGRectContainsPoint(hitFrame, point);
}
static char hitTestEdgeInsetsKey;
- (void)setHitTestEdgeInsets:(UIEdgeInsets)hitTestEdgeInsets {
objc_setAssociatedObject(self, &hitTestEdgeInsetsKey, [NSValue valueWithUIEdgeInsets:hitTestEdgeInsets], OBJC_ASSOCIATION_RETAIN);
}
- (UIEdgeInsets)hitTestEdgeInsets {
return [objc_getAssociatedObject(self, &hitTestEdgeInsetsKey) UIEdgeInsetsValue];
}
void Swizzle(Class c, SEL orig, SEL new) {
Method origMethod = class_getInstanceMethod(c, orig);
Method newMethod = class_getInstanceMethod(c, new);
if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)))
class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
else
method_exchangeImplementations(origMethod, newMethod);
}
@end
The nice thing about this approach is you can use this even in Storyboards by adding a User Defined Runtime Attribute. Sadly, UIEdgeInsets
is not directly available as a type there but since CGRect
also consists of a struct with four CGFloat
it works flawlessly by choosing "Rect" and filling in the values like this: {{top, left}, {bottom, right}}
.
Upvotes: 7
Reputation: 23976
I'm using the following class in Swift, to also enable an Interface Builder property to adjust the margin:
@IBDesignable
class ALExtendedButton: UIButton {
@IBInspectable var touchMargin:CGFloat = 20.0
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
var extendedArea = CGRectInset(self.bounds, -touchMargin, -touchMargin)
return CGRectContainsPoint(extendedArea, point)
}
}
Upvotes: 6
Reputation: 4152
@jlajlar 's answer above seemed good and straightforward but does not match Xamarin.iOS, so I converted it into Xamarin. If you looking for a solution on a Xamarin iOS, there here it goes:
public override bool PointInside (CoreGraphics.CGPoint point, UIEvent uievent)
{
var margin = -10f;
var area = this.Bounds;
var expandedArea = area.Inset(margin, margin);
return expandedArea.Contains(point);
}
You can add this method to the class where you are overriding UIView or UIImageView. This worked nicely :)
Upvotes: 0
Reputation: 1358
I am so late to this game, but wanted to weigh in on a simple technique that might solve your problems. Here is a typical programmatic UIButton snippet for me:
UIImage *arrowImage = [UIImage imageNamed:@"leftarrow"];
arrowButton = [[UIButton alloc] initWithFrame:CGRectMake(15.0, self.frame.size.height-35.0, arrowImage.size.width/2, arrowImage.size.height/2)];
[arrowButton setBackgroundImage:arrowImage forState:UIControlStateNormal];
[arrowButton addTarget:self action:@selector(onTouchUp:) forControlEvents:UIControlEventTouchUpOutside];
[arrowButton addTarget:self action:@selector(onTouchDown:) forControlEvents:UIControlEventTouchDown];
[arrowButton addTarget:self action:@selector(onTap:) forControlEvents:UIControlEventTouchUpInside];
[arrowButton addTarget:self action:@selector(onTouchUp:) forControlEvents:UIControlEventTouchDragExit];
[arrowButton setUserInteractionEnabled:TRUE];
[arrowButton setAdjustsImageWhenHighlighted:NO];
[arrowButton setTag:1];
[self addSubview:arrowButton];
I'm loading a transparent png Image for my button and setting the background image. I'm setting the frame based on the UIImage and scaling by 50% for retina. OK, maybe you agree with the above or not, BUT if you want to make the hit area BIGGER and save yourself a headache:
What I do, open the image in photoshop and simply increase the canvas size to 120% and save. Effectively you've just made the image bigger with transparent pixels.
Just one approach.
Upvotes: 0
Reputation: 13181
There is nothing wrong with the answers presented; however I wanted to extend jlarjlar's answer as it holds amazing potential that can add value to the same problem with other controls (e.g. SearchBar). This is because since pointInside is attached to a UIView, one is able to subclass any control to improve the touch area. This answer also shows a full sample of how to implement the complete solution.
Create a new subclass for your button (or any control)
#import <UIKit/UIKit.h>
@interface MNGButton : UIButton
@end
Next override the pointInside method in your subclass implementation
@implementation MNGButton
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
//increase touch area for control in all directions by 20
CGFloat margin = 20.0;
CGRect area = CGRectInset(self.bounds, -margin, -margin);
return CGRectContainsPoint(area, point);
}
@end
On your storyboard/xib file select the control in question and open the identity inspector and type in the name of your custom class.
In your UIViewController class for scene containing the button, change the class type for the button to the name of your subclass.
@property (weak, nonatomic) IBOutlet MNGButton *helpButton;
Link your storyboard/xib button to the property IBOutlet and your touch area will be expanded to fit the area defined in the subclass.
In addition to overriding the pointInside method together with the CGRectInset and CGRectContainsPoint methods, one should take time to examine the CGGeometry for extending the rectangular touch area of any UIView subclass. You may also find some nice tips on CGGeometry use-cases at NSHipster.
For example one could make the touch area irregular using the methods mentioned above or simply choose to make the width touch area twice as large as the horizontal touch area:
CGRect area = CGRectInset(self.bounds, -(2*margin), -margin);
NB: Substituting any UI Class control should produce similar results on extending the touch area for different controls (or any UIView subclass, like UIImageView, etc).
Upvotes: 12
Reputation: 4519
Never override method in category. Subclass button and override - pointInside:withEvent:
. For example if your button's side is smaller than 44 px (which is recommended as minimum tappable area) use this:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
return (ABS(point.x - CGRectGetMidX(self.bounds)) <= MAX(CGRectGetMidX(self.bounds), 22)) && (ABS(point.y - CGRectGetMidY(self.bounds)) <= MAX(CGRectGetMidY(self.bounds), 22));
}
Upvotes: 2
Reputation: 71
Don't change the behavior of UIButton.
@interface ExtendedHitButton: UIButton
+ (instancetype) extendedHitButton;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
@end
@implementation ExtendedHitButton
+ (instancetype) extendedHitButton {
return (ExtendedHitButton *) [ExtendedHitButton buttonWithType:UIButtonTypeCustom];
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGRect relativeFrame = self.bounds;
UIEdgeInsets hitTestEdgeInsets = UIEdgeInsetsMake(-44, -44, -44, -44);
CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, hitTestEdgeInsets);
return CGRectContainsPoint(hitFrame, point);
}
@end
Upvotes: 7
Reputation: 4169
Don't set the backgroundImage
property with your image, set the imageView
property. Also, make sure you have imageView.contentMode
set at UIViewContentModeCenter
.
Upvotes: 19
Reputation: 191
I have followed Chase's response and it works great, one single problem when you create the arrea too big, bigger than the zone where the button gets deselected (if the zone wasn't bigger) it doesn't call the selector for the UIControlEventTouchUpInside event.
I think the size is over 200 any any direction or something like that.
Upvotes: 0
Reputation: 2260
I've been able to increase the hit area of the info button programmatically. The "i" graphic doesn't change scale and remains centered in the new button frame.
The size of the info button seems to be fixed to 18x19[*] in Interface Builder. By connecting it to an IBOutlet, I was able to change its frame size in code without any issues.
static void _resizeButton( UIButton *button )
{
const CGRect oldFrame = infoButton.frame;
const CGFloat desiredWidth = 44.f;
const CGFloat margin =
( desiredWidth - CGRectGetWidth( oldFrame ) ) / 2.f;
infoButton.frame = CGRectInset( oldFrame, -margin, -margin );
}
[*]: Later versions of iOS appear to have increased the hit area of the info button.
Upvotes: 5