bdunagan
fill the void

Posted
1 March 2010 @ 10pm

Tagged
development

9 Comments

iPhone Tip: Larger Hit Area for UIButton

2010-08-18 Update: I added a section explaining how to programmatically grow the touchable area of a UIButton via hitTest.

Apple’s iPhone HIG estimates 44×44 pixels is a good hit area for a UI element. If the button isn’t that big, don’t let the user realize that: make the hit area larger than the button.

The UINavigationItem button is an excellent example of this larger hit area. Try touching near (but not on) the standard “Back” button in any navigation-based iPhone app. It responds. I never even noticed this interaction until I started this post, and that’s precisely the goal.

But when Apple doesn’t auto-extend the hit area, the developer should. There are two approaches:

A Larger Target

I can programmatically grow the area of the button by subclassing UIButton and overriding UIView::hitTest:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
	int errorMargin = 30;
	CGRect largerFrame = CGRectMake(0 - errorMargin, 0 - errorMargin, self.frame.size.width + errorMargin, self.frame.size.height + errorMargin);
	return (CGRectContainsPoint(largerFrame, point) == 1) ? self : nil;
}

An Invisible Button

For the invisible button, use Interface Builder to create a large UIButtonTypeCustom UIButton, hook up its touchDown, touchUpInside, touchUpOutside events to custom selectors in the controller, and use UIButton::setHighlighted on the original button to mimic a real tap, as described in this spot-on StackOverflow comment. (And don’t try setting the button’s alpha to zero; the button will no longer respond to touch events.)

Either way, an oversized hit area is especially necessary for smaller buttons, like the 29×31 “Detail Disclosure” button (UIButtonTypeDetailDisclosure) or the 18×19 “Info Light” button (UIButtonTypeInfoLight). Try simply adding the “Info Light” i button to a blank screen, running it on a device, and tapping it; it’s frustratingly small.


9 Comments

Posted by
Michelle
1 April 2010 @ 12pm

Can you say a bit about how to “programmatically grow button’s hit area via UIView::hitTest”. I’m not clear how you can do that for a UIButton, as buttonWithType always returns a UIButton rather than a subclass.


Posted by
bdunagan
18 August 2010 @ 7pm

@Michelle No need to worry about buttonWithType. If you simply subclass UIButton and override hitTest, it just works. I updated the post with code showing how.


Posted by
kb1ooo
8 February 2011 @ 9pm

@bdunagan what do you mean “it just works”. You didn’t show how you created an instance of your subclass? If you are *not* using +buttonWithType then you are stuck at only being able to create UIButtonTypeCustom type buttons. What do you do for the other types? It’s worse than @Michelle suggested because +buttonWithType returns an instance of one of many different private classes depending on the type.


Posted by
kb1ooo
8 February 2011 @ 9pm

@bdunagan one correction. +buttonWithType (in addition to using -initWithFrame) will return the proper subclass, but again *only* for UIButtonTypeCustom.


Posted by
Tyler
22 November 2011 @ 6pm

Instead of using CGRectMake, you could use CGRectInset([self frame],-errorMargin,-errorMargin).

It’s a bit cleaner.


Posted by
Tyler
22 November 2011 @ 6pm

Nevermind. It behaves in unexpected ways. Ah well.


Posted by
Tyler
22 November 2011 @ 6pm

Actually, using pointInside works better, as your hitTest routine is overriding the isHidden flag. Your button still works even if it is hidden.

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
CGRect rect = CGRectInset([self bounds], -margin, -margin);
return CGRectContainsPoint(rect, point);
}


Posted by
Marc
9 January 2012 @ 2am

There’s a flaw in the maths here. You’re subtracting the error margin from the origin, and then adding it to the width and height. This cancels out and means it won’t recognise hits below the button or to the right.

The line:
CGRect largerFrame = CGRectMake(0 – errorMargin, 0 – errorMargin, self.frame.size.width + errorMargin, self.frame.size.height + errorMargin);

Should be replaced by:
CGRect largerFrame = CGRectMake(0 – errorMargin, 0 – errorMargin, self.frame.size.width + (errorMargin * 2), self.frame.size.height + (errorMargin * 2));


Posted by
Mike Weller
16 May 2012 @ 9am

Using pointInside doesn’t work. The button will highlight etc. but its target/selector are not invoked for UIControlEventTouchUpInside.


Leave a Comment