Custom UIGestureRecognizer with 3D Touch

Hey guys,


I'm struggling with building a custom UIGestureRecognizer using 3D Touch. I subclassedUIGestureRecognizer according to the documentation, but regrettably any touch, whether its force passes the specified threshold or not, sends an action message to the target. Even if the gesture isn't interpreted successfully! Whensoever I tap, the target's selector is executed. My implementation of the overridden methods reads as follows:

- (id)initWithTarget:(id)target action:(SEL)action threshold:(CGFloat)threshold {
    self = [super initWithTarget:target action:action];
    if (self) {
        [self setThreshold:threshold];
    }
    return self;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    self.state = UIGestureRecognizerStateBegan;
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesMoved:touches withEvent:event];
    UITouch *touch = touches.anyObject;
    if (touch) {
        CGFloat recognizedForceNormalized = touch.force/touch.maximumPossibleForce;
        if (recognizedForceNormalized < 1.0  && recognizedForceNormalized <= self.getThreshold) {
            // is the recognized force (normalized) smaller 1.0 (our max. possible value) and 
            // unequal the specified threshold?
            self.state = UIGestureRecognizerStateChanged;
        }
        if (recognizedForceNormalized >= self.getThreshold) {
            self.state = UIGestureRecognizerStateRecognized;
        }
    }
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesEnded:touches withEvent:event];
    self.state = UIGestureRecognizerStateEnded;
}

Does anyone of you have an idea what the matter with this implementation is? I really appreciate any help you can provide!!


Kind regards,


Alex

Answered by Poets in 69122022

Switching to the "Cancelled" state during touchesEnded: did the trick.

I believe your error is in switching to the "Began" state during touchesBegan. This indicates that you have recognized your gesture and that we should send gesture events each time.


What you want to do is leave the state in possible and only once you get past the threshold do you send "Recognized".

Thank you for your help, I changed that. But regrettably that didn't do the trick; maybe the cause of the problem is my use of the custom UIForceTouchGestureRecognizer object. That's how I use the class:

if (self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable) {
        UIForceTouchGestureRecognizer *ftgr = [[UIForceTouchGestureRecognizer alloc]
                    initWithTarget:self action:@selector(forcePress:) threshold:0.8];
        [cell addGestureRecognizer:ftgr];
    }

This is the implementation of the target method:

- (void)forcePress:(UIForceTouchGestureRecognizer *)gesture {
    if (gesture.state == UIGestureRecognizerStateRecognized) {
        UITableViewCell *cell = (UITableViewCell *)[gesture view];
        [self actionForRecognizedGesture:cell];
    }
}

Is anything wrong about this approach? Thanks for your help again.


Kind regards,


Alex

I don't immediately see anything wrong, but a force threshold if 0.8 is rather low – the standard touch value of 1.0 represents the typical force that a touch has, so using something lighter is likely to result in false positives for you.


Also you don't really have to check if you have force before creating your gesture – if there is no force available, you'll just always see 0. Its not wrong to do this kind of preflighting, but in many cases it is unnecessary.

0.8 isn't the actual value of the force – it's 80 percent of the maximum possible force, so that I don't suppose it's likely to result in false positives. This can't be the cause of the problem either as I 'normalize' the input force; see the following line of code (as posted above):

CGFloat recognizedForceNormalized = touch.force/touch.maximumPossibleForce;

Hmm, I really am clueless about this thing ...


Kind regards,


Alex

Sorry I missed that. That said, the maximum force is actually fairly high – perhaps your gesture is failing before you can reach that high?


Honestly working in terms of maximum possible force isn't really a good idea as it can make your application feel different on different devices. The intent of the standard force being 1.0 (rather than a fraction of maximum) is that it means that by default if you work in terms of that force your force interactions should feel roughly the same on all devices.

But if my gesture is failing before I can reach that high the action messages rather shouldn't get sent whenever I tap, am I getting this right? Possibly there's still a bug in the implementation of my subclass ... 😕 When I debug the implementation of UIForceTouchGestureRecognizer and set a breakpoint inside the following if-loop the action message is sent although the state didn't even change to 'recognized'. The state just changes to 'recognized' if the threshold is actually passed. Despite that the action message is getting sent even if the force of the touch doesn't pass the threshold and the state doesn't change to 'recognized'. This results in every touch getting interpreted as gesture. Knowing that still doesn't help me concerning the bug. Do you might have a clue?

if (recognizedForceNormalized >= inputThreshold) {
    self.state = UIGestureRecognizerStateRecognized;
}


Kind regards,


Alex

Accepted Answer

Switching to the "Cancelled" state during touchesEnded: did the trick.

Custom UIGestureRecognizer with 3D Touch
 
 
Q