4
votes

I am trying to create a custom UIControl similar to a slider.

This control is to be the subview of a view that also has a tap gesture recognizer attached to it.

The problem now is that this tap gesture recognizer cancels the touches sent to my control. Is there a way I can override this from within the code of my control?

If I look into the standard controls in iOS it looks as if UIButton has a way of overriding the tap gesture recognizer but UISlider doesn't. So if I replace my custom control with a UIButton the tap gesture recognizer does not trigger its action, but if I replace it with a slider it does.

edit: I made a small project in Xcode to play around in. Download here https://dl.dropboxusercontent.com/u/165243/TouchConcept.zip and try to change it so that

  • The UICustomControl does not know about the tap gesture recognizer
  • The UICustomControl is not cancelled when the user taps down on the yellow box
  • The UICustomControl does not inherit from UIButton (that is a solution that does not feel right and might give me more headaches later on)

The code:

// inherit from UIButton will give the wanted behavior, inherit from UIView (or UIControl) gives
// touchesCancelled by the gesture recognizer
@interface UICustomControl : UIView

@end

@implementation UICustomControl

-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{   NSLog(@"touchesBegan"); }

-(void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{   NSLog(@"touchesMoved"); }

-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{   NSLog(@"touchesEnded"); }

-(void) touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{   NSLog(@"touchesCancelled"); }

@end
@interface ViewController ()

@end


@implementation ViewController

- (void)viewDidLoad
{
    UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(logTap:)];
    [self.view addGestureRecognizer:tapRecognizer];
    UIView *interceptingView = [[UICustomControl alloc]initWithFrame:CGRectMake(10, 10, 100, 100)];
    interceptingView.userInteractionEnabled = YES;
    interceptingView.backgroundColor = [UIColor yellowColor];
    [self.view addSubview: interceptingView];
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
}

- (void) logTap: (id) sender
{
    NSLog(@"gesture recognizer fired");
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end
4
When I overrode a UIControl I had to roll my own tracking by overriding UIResponder's touchesBegan/Moved/Ended/Canceled:withEvent: and not calling super. I gave up using begin/continue/endTrackingWithTouch:withEvent: because it was problematic - too much stuff going on behind the scenes in there. HTH. - t0rst
Sample project works fine: touchesBegan,Moved,Ended is not interfered with by gesture recogniser (hence you can track movement of your slider), but touch without moving gets swallowed by the tap recogniser. "to be the subview of a view that also has a tap gesture recognizer attached to it" implies EVERY tap in view must be recognised as a tap regardless of subviews, but that is plainly not what you want. Instead of using tap gesture recogniser, why not use touchesBegan/Moved/Ended/Canceled:withEvent: in your view controller (=super in responder chain) to trap otherwise unhandled taps? - t0rst
I verified again and for me (iPad simulator 6.1) it does NOT work as expected: the tap gesture recognizer fires every time, touchesCancelled is called every time and I never get touchesEnded. Your proposed solution is a workaround. At the moment I am going with another workaround: inheriting from UIButton. - Kristof Van Landschoot
2013-09-04 10:20:27.287 TouchConcept[12033:c07] touchesBegan 2013-09-04 10:20:31.135 TouchConcept[12033:c07] touchesMoved 2013-09-04 10:20:31.184 TouchConcept[12033:c07] touchesMoved 2013-09-04 10:20:31.999 TouchConcept[12033:c07] touchesMoved 2013-09-04 10:20:32.959 TouchConcept[12033:c07] touchesMoved 2013-09-04 10:20:36.167 TouchConcept[12033:c07] touchesEnded - t0rst
behaviour is flakey and unreliable: if you move and touch up quickly the GR fires, but if you move slowly and touch up slowly, it doesn't fire. - t0rst

4 Answers

5
votes

You can configure the gesture recognizer to not cancel touches in the view it's attached using the "cancels touches in view" property:

myGestureRecognizer.cancelsTouchesInView = NO;
5
votes

I'm a little bit late, but for those (like me) stumbling into this question, I used an alternative solution:

Use the delegate of the gesture recogniser.

UITapGestureRecognizer *tapGestRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissInfoBox:)];
tapGestRec.delegate = self;
[self.view addGestureRecognizer:tapGestRec];

Then do a sort of hit test in the shouldReceiveTouch delegate function when the gesture recogniser wants to handle/swallow a touch.

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    CGPoint location = [touch locationInView:self.view];
    return !CGRectContainsPoint(self.myCustomControl.frame, location) && !CGRectContainsPoint(self.myOtherCustomControl.frame, location);
}

I did all this in my ViewController, this way the UIControl does not have to know about any sibling views and the gesture recogniser does not 'steal' taps from my custom controls and only handles 'uncaught' taps.

Also, this way you won't trigger both the gesture recogniser and the custom control, which would happen with cancelsTouchesInView.

BTW, maybe it works with UIButton because UIButton uses gesture recognisers internally? I think they understand each other, while UIControls and recognisers do not. Not sure though.

3
votes

override gestureRecognizerShouldBegin(_:) in your UIControl subclass.

    public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        if gestureRecognizer.isKind(of: UITapGestureRecognizer.self) {
            return false
        } else {
            return super.gestureRecognizerShouldBegin(gestureRecognizer)
        }
    }