1

In my application I have a UIButton that is quite small, so I thought about increasing the hit area of it.

I found an extension for that:

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

but when I use it, every button in my app has a bigger hit area. I want to increase it to only one specialButton - how can I do it?

ronatory
  • 7,156
  • 4
  • 29
  • 49
user3766930
  • 5,629
  • 10
  • 51
  • 104
  • An alternative to @ronatory's answer is to (1) subclass UIButton as, say, `SpecialButton`, and then extend that instead of UIButton. –  Feb 11 '17 at 22:17
  • Note that overriding functions from an extension is a bad idea, and is not allowed in "pure" swift, only in Objective-C NSObject classes. To quote the Apple Swift iBook: “Extensions can add new functionality to a type, but they cannot override existing functionality” Excerpt From: Apple Inc. “The Swift Programming Language (Swift 3.0.1).” iBooks. https://itun.es/us/jEUH0.l – Duncan C Feb 11 '17 at 22:23
  • Updated my answer. But unfortunately the method does not work like expected. I think you should try matt's answer http://stackoverflow.com/a/42182054/5327882 – ronatory Feb 11 '17 at 23:28
  • @ronatory thanks for your answer though, I really appreciate your effort :) The thing is I tried matt's answer and it doesn't seem to work well, I'm not sure what might be the problem there though – user3766930 Feb 11 '17 at 23:51

3 Answers3

1

Don't expand the hit area; shrink the drawing area. Make the button a subclass of UIButton, and in that subclass, implement rect methods, along these lines:

class MyButton : UIButton {
    override func contentRect(forBounds bounds: CGRect) -> CGRect {
        return bounds.insetBy(dx: 30, dy: 30)
    }
    override func backgroundRect(forBounds bounds: CGRect) -> CGRect {
        return bounds.insetBy(dx: 30, dy: 30)
    }
}

Now the button is tappable 30 points outside its visible background.

enter image description here

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Currently my button has constraints set like this: http://imgur.com/a/T3p8x if I want to expand the touch area for e.g. 5 from each side, how should I do it? Do I have to change the `width` and `height` in constraints (by adding 5 to each of them) and then how could I change the `contentEdgeInsets`? – user3766930 Feb 11 '17 at 22:45
  • Sorry, my first answer didn't work, but my second answer does. – matt Feb 11 '17 at 22:56
  • hmm @matt, I tried your approach, put this class as an outlet connection and also set it up in storyboard for my button, but it doesn't work well - currently the image from my button is gone... the button itself is clickable, but the area is not expanded for sure for 30px more each side :( – user3766930 Feb 11 '17 at 23:32
  • The clickable area is the size of the button. You said you have constraints so you will need to adjust those, obviously. – matt Feb 12 '17 at 00:35
  • hm, so I changed the constraints now of my button to `width: 45` and `height:45`, then ran the app and the button changed size... what am I doing wrong? I expected the button to be the same size, but the clickable area grow around it... My button has a corner radius set up and it's round - maybe that causes some problems here? – user3766930 Feb 12 '17 at 00:43
  • I can't see what you're now doing. But I assure you that what I describe works. – matt Feb 12 '17 at 01:17
  • Added an animated gif showing that the button is tappable 30 pixels outside its visible boundary. – matt Feb 12 '17 at 01:24
0

You can add a computed property to your extension which you can set in your controller if you want to enable the hit area like this:

fileprivate let minimumHitArea = CGSize(width: 100, height: 100)

extension UIButton {

  var isIncreasedHitAreaEnabled: Bool {
    get {
      // default value is false, so you can easily handle from outside when to enable the increased hit area of your specific button
      return false
    }
    set {

    }
  }

  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 }

    // only if it's true the hit area can be increased
    if isIncreasedHitAreaEnabled {
      // 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
    }
    return self
  }
}

And then in your view controller just set isIncreasedHitAreaEnabled to true if you want to increase it for a specific button (e.g. in your viewDidLoad):

override func viewDidLoad() {
  super.viewDidLoad()
  specialButton.isIncreasedHitAreaEnabled = true
}

Update:

First of all the method which you use doesn't work like expected. And also my approach with the computed property did not work like I thought. So even if the method does not work like expected, I just want to update my approach with the computed property. So to set the property from outside, you can use Associated Objects. Also have look here. With this solution now it would be possible to handle the Bool property isIncreasedHitAreaEnabled from your controller:

fileprivate let minimumHitArea = CGSize(width: 100, height: 100)
private var xoAssociationKey: UInt8 = 0

extension UIButton {

  var isIncreasedHitAreaEnabled: Bool {
    get {
      return (objc_getAssociatedObject(self, &xoAssociationKey) != nil)
    }
    set(newValue) {
      objc_setAssociatedObject(self, &xoAssociationKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
    }
  }

  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 }

    if isIncreasedHitAreaEnabled {
      // 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
    }
    return self
  }
}
Community
  • 1
  • 1
ronatory
  • 7,156
  • 4
  • 29
  • 49
  • so let me get this right - if I don't set the flag `isincreasedHitAreaEnabled`, then it will be false by default? I mean - I do not need to set it to false on every other button in my app, right? – user3766930 Feb 11 '17 at 22:01
  • hmm, @ronatory, I think this code affected all buttons in my app, even though I set `isIncreased...` only on one of them... What is more, even if I change the `minimumHitArea` to `width:10, height:10`, it seems like the hit area covers the whole view - do you know what might be wrong here? – user3766930 Feb 11 '17 at 22:20
  • I will check it – ronatory Feb 11 '17 at 22:24
0

Create a Custom UIButton Class and override pointInside:(CGPoint)point method like below. Then set these properties value from viewController.

#import <UIKit/UIKit.h>
@interface CustomButton : UIButton
    @property (nonatomic) CGFloat leftArea;
    @property (nonatomic) CGFloat rightArea;
@property (nonatomic) CGFloat topArea;
@property (nonatomic) CGFloat bottomArea;
@end




#import "CustomButton.h
@implementation CustomButton

-(BOOL) pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    CGRect newArea = CGRectMake(self.bounds.origin.x - _leftArea, self.bounds.origin.y - _topArea, self.bounds.size.width + _leftArea + _rightArea, self.bounds.size.height + _bottomArea + _topArea);

    return CGRectContainsPoint(newArea, point);
}
@end