7

I have a view with a tap gesture recognizer. A subview of this view is an instance of my custom class, which inherits from UIControl. I am having an issue where the UIControl subclass will sometimes allow touch events to pass through to the parent view when it shouldn't.

Within the UIControl subclass, I have overridden these functions (code is in Swift)

override func beginTrackingWithTouch(touch: UITouch, withEvent event: UIEvent) -> Bool
{
    return true
}

override func continueTrackingWithTouch(touch: UITouch, withEvent event: UIEvent) -> Bool
{
    // The code here moves this UIControl so its center is at the touchpoint
    return true
}

override func endTrackingWithTouch(touch: UITouch,withEvent event: UIEvent)
{
    // Something important happens here!
}

This system works just fine if the user touches down within the UIControl, drags the control around in both X and Y directions, and then lifts off the screen. In this case, all three of these functions are called, and the "something important" happens.

However, if the user touches down with the UIControl, drags the control around only in the X direction, and then lifts off the screen, we have a problem. The first two functions are called, but when the touchpoint lifts off the screen, the tap gesture recognizer is called, and endTrackingWithTouch is not called.

How do I make sure that endTrackingWithTouch is always called?

jeremywhuff
  • 2,911
  • 3
  • 29
  • 33

4 Answers4

5

I fixed this in a way that I consider to be a hack, but there's really no alternative, given how UIGestureRecognizer works.

What was happening was that the tap gesture recognizer was canceling the control's tracking and registering a tap gesture. This was because when I was dragging horizontally, I just happened to be dragging short distances, which gets interpreted as a tap gesture.

The tap gesture recognizer must be disabled while the UIControl is tracking:

override func beginTrackingWithTouch(touch: UITouch, withEvent event: UIEvent) -> Bool
{
    pointerToSuperview.pauseGestureRecognizer()
    return true
}

override func continueTrackingWithTouch(touch: UITouch, withEvent event: UIEvent) -> Bool
{
    // The code here moves this UIControl so its center is at the touchpoint
    return true
}

override func endTrackingWithTouch(touch: UITouch,withEvent event: UIEvent)
{
    // Something important happens here!
    pointerToSuperview.resumeGestureRecognizer()
}

override func cancelTrackingWithEvent(event: UIEvent?)
{
    pointerToSuperview.resumeGestureRecognizer()
}

In the superview's class:

pauseGestureRecognizer()
{
    tapGestureRecognizer.enabled = false
}

resumeGestureRecognizer()
{
    tapGestureRecognizer.enabled = true
}

This works because I'm not dealing with multitouch (it's OK for me not to receive tap touch events while tracking touches with the UIControl).

Ideally, the control shouldn't have to tell the view to pause the gesture recognizer - the gesture recognizer shouldn't be meddling with the control's touches to begin with! However, even setting the gesture recognizer's cancelsTouchesInView to false cannot prevent this.

jeremywhuff
  • 2,911
  • 3
  • 29
  • 33
  • Good answer. Sometimes instead of disabling the gesture you can use the gesture's delegate method: optional func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool – viggio24 Sep 01 '15 at 12:22
  • If you implement your own TapGestureRecognizer and add it to your control, it catches the taps before any superview does. That way you don't need to know anything about other recognizers in the view hierarchy. Details below... – Adam Wilt Mar 26 '17 at 00:23
2

There's a way to fix this that's nicely self-contained: instantiate your own TapGestureRecognizer and attach it to your custom control, e.g. in Objective-C,

_tapTest = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)];
_tapTest.numberOfTapsRequired = 1;
[self addGestureRecognizer:_tapTest];

and then implement the tapped action handler to process the tap:

- (void)tapped:(UITapGestureRecognizer *)recognizer {...}

In my case, I handle tapped the same as endTrackingWithTouch:withEvent:; your mileage may vary.

This way, you get the tap before any superview can snatch it, and you don't have to worry about the view hierarchy behind your control.

Adam Wilt
  • 553
  • 4
  • 10
  • This fixed it. I'm still puzzled by why the gesture recognizer interferes with custom `UIControl` subclasses, but **not** with `UIButton` or `UITextField` (I only noticed the issue when I replaced a UIButton with my custom checkbox control...). This answer fixes irt for me, but the stock controls don't seem to need it in order to work somewhow... – Nicolas Miari Feb 19 '19 at 10:35
1

When a UIControl is moved while tracking touches, it might cancel its tracking. Try overriding cancelTrackingWithEvent and see if this is the case. If you do see the cancel, you're going to have to track your touches in an unmoving view somewhere in the parent hierarchy of this control.

Ian MacDonald
  • 13,472
  • 2
  • 30
  • 51
  • That ended up being the issue! It's awfully strange that it only triggers the cancel when dragging horizontally, but I just had to work around it by making a full screen UIControl with a UIView subview that gets moved around (instead of the UIControl itself). – jeremywhuff Sep 22 '14 at 20:42
  • Actually, I take that back. The tracking event was not getting canceled because I was changing the UIControl's frame. It was because whenever I dragged horizontally, I just happened to be dragging short distances. This was triggering the gesture recognizer, since it was interpreted as a tap, and it canceled the tracking event. – jeremywhuff Sep 23 '14 at 23:04
1

I know this is old, but I run into the same problem, check if one of your superviews has gesture recogniser, and deactivate them when you need to use the UIControl.

I actually ended changed the superview of the UIControl to the main window to avoid this conflicts (Because it was in a popup).

ZiggyST
  • 587
  • 6
  • 11