Human Interface Guidelines compliant UIButton subclass

iOS Human Interface Guidelines tell us:

Make it easy for people to interact with content and controls by giving each interactive element ample spacing. Give tappable controls a hit target of about 44 x 44 points.

Sometimes icons for the buttons, provided by designers, are smaller than the above mentioned 44 x 44 points size (44 x 44 non-retina, 88 x 88 retina).

There are several ways to solve this guideline inconsistency:

  • Don't do anything - the App will look bad and feel bad - the user won't be able to tap on tiny buttons.
  • Make the designer change it - sometimes the designer is unavailable; it's a bother to ask the designer to do this if he is not in your office, sitting next to you; sometimes the button really looks good smaller.
  • Stretch the icon - the icon will look pixelated or blurred; we should aim for being pixel perfect in our Apps.
  • Override the pointInside:withEvent: method.

According to the docs, the pointInside:withEvent: method

returns a Boolean value indicating whether the receiver contains the specified point.

We can use this method, to enlarge the area where the button captures touches (we still have to remember that because of how the view hierarchy works in iOS, the touch has to be in the bounds of the superview of our view).

- (BOOL)pointInside:(CGPoint)point 
		  withEvent:(UIEvent *)event {

const static CGFloat minimumSide = 44;
CGFloat differenceY = minimumSide - self.bounds.size.height;
CGFloat differenceX = minimumSide - self.bounds.size.width;

CGFloat insetY = MAX(0, differenceY);
CGFloat insetX = MAX(0, differenceX); 

return CGRectContainsPoint(CGRectInset(self.bounds, -insetX, -insetY), point) || [super pointInside:point withEvent:event];
}
  • We use self.bounds instead of self.frame because the documentation states:

point - A point that is in the receiver’s local coordinate system (bounds).

  • We use CGRectContainsPoint(rect, point) to do a simple geometric check if the point is inside of the rect.

  • We use CGRectInset(rect, dx, dy) to create a bigger rectangle to check if the point is contained in it. dx and dy are the differences between 44 and width or height accordingly. CGRectInset returns a rectangle with the same center point, so we don't have to do math ourselves.

  • We call super for safety reasons. We don't know if Apple isn't using some hack now or in future iOS version, so better safe than sorry. Theoretically there should be no reason to call it, as we already check if the point is in the bounds of the (even bigger) button.

I use it by creating a thin UIButton subclass, for example ABOutsideTouchButton and just use this as a superclass of most of the interface elements.