How do I programmatically end/reset a UIGestureRecognizer? - ios

Say I am currently tracking a drag gesture. In my event handler I use a threshold to determine when the drag results in an action. When the threshold is crossed, I want to indicate that the drag gesture has completed.
The only thing I can find in the docs is this line here:
If you change this property to NO while a gesture recognizer is
currently recognizing a gesture, the gesture recognizer transitions to
a cancelled state.
So:
if (translation.y > 100) {
// do action
[self doAction];
//end recognizer
sender.enabled = NO;
sender.enabled = YES;
}
This works but it looks like there might be a neater way.
Does anyone know of another way to indicate that a gesture has ended programmatically? I would expect something like a method -end: that generates a final event with state UIGestureRecognizerStateEnded.

Have you defined a custom UIGestureRecognizer? If the gesture you're recognizing is different from the standard ones defined by Apple because it has a different threshold or is otherwise not the same as a regular UIPanGestureRecognizer, then it might make sense to create your own UIGestureRecognizer. (see subclassing notes)
If you have subclassed UIGestureRecognizer, you can simply set the state like this:
self.state = UIGestureRecognizerStateEnded;
You probably want to do this in the touchesMoved:withEvent: method. Also note:
"Subclasses of UIGestureRecognizer must import UIGestureRecognizerSubclass.h. This header file contains a redeclaration of state that makes it read-write."
On the other hand, if you're only implementing a UIGestureRecognizerDelegate, the state is read-only, and there is no way to directly set it. In that case your method of disabling/enabling might be the best you can do.

With the code you showed you need to have the logic for starting the animation when the gesture recognizer is canceled and I'd say this is not good as there are other ways that this gesture recognizer can be canceled without you wanting to have the animation done.
Considering you have a method for starting the animation you just need to call this method when the threshold is passed and when the gesture ends normally. Two different occasions then.
The code you presented would look like this:
if (translation.y > 100) {
// do action
[self finishFlip];
sender.enabled = NO;
sender.enabled = YES;
}
Canceling the gesture here may be useful also if that prevents any following actions if the user keeps dragging its finger.
If you'll have a team developing over this and you need a specific event to happen you should subclass the gesture recognizer as nont suggested.

Related

UITextInteraction + UIScrollView

I have a custom text input view that adopts the UITextInput protocol. This view is embedded within a UIScrollView. The UITextinput adopting view includes UITextInteraction. When I drag the selection handles to the edges (top or bottom) of the current visible area, the view should scroll and select text automatically (like UITextView). Does anybody know how to achieve this?
I was hoping that UITextInteraction might inform me via its delegate of necessary events, but it does not (or even takes care of this functionality automatically).
I have tried to intercept closestPosition(to point: CGPoint) -> UITextPosition?, which is called whenever the user touches the UITextInput adopting view. Therefore, I can use it to track the dragging operation of a selection handle. Once, the user reaches the top of the view, I scroll up. However, I cannot detect when the user lets go of the handle (when touch ends). When this happens, the scrollView should stop scrolling. In my case, the scroll view keeps scrolling to the top.
I also tried to intercept selectionRects(for range: UITextRange) -> [UITextSelectionRect], but it is called sporadically during a scrolling.
I also cannot detect touchesEnded(). The UITextInteraction seems to block the call. Further, I cannot implement my own pan gesture. UITextInteraction blocks this as well during a selection operation.
Has anybody successfully used UITextInteraction? It seems very pre-mature at this stage.
Here's the way I got this info out of UITextInteraction for the text insertion point placement. I have not yet attempted to track the selection handles, but will update the answer if I manage it
UITextInteraction has a property called gesturesForFailureRequirements. If you dump the content of these you'll notice one named UIVariableDelayLoupeGesture which is a subclass of UILongPressGestureRecognizer. This is the one we want when the use "picks up" the cursor with their finger
I add my own target/selector to this gesture recogniser when adding my text interaction like so:
for gesture in interaction.gesturesForFailureRequirements {
if gesture.isKind(of: UILongPressGestureRecognizer.self) {
gesture.addTarget(self, action: #selector(longPressEdgeScrollGesture(_:)))
}
}
Then in longPressEdgeScrollGesture you can get the coordinates of the cursor in your scroll view to activate your edge scrolling timer as necessary
#objc private func longPressEdgeScrollGesture(_ gesture: UILongPressGestureRecognizer) {
guard gesture.numberOfTouches > 0 else {
return
}
let location = gesture.location(in: gesture.view)
print(location)
//Start/stop your edge scrolling as required
}
I found a solution to my original problem. It is somewhat of a work around, but hopefully Apple will improve UITextInteraction.
Trying to intercept any functions in UITextInput led nowhere. Thankfully some very clever people on Twitter figured out what to do. You need to intercept the right UIGestureRecognizer that is added automatically. Much like Simeon explained in his answer. The recognizer in question is called UITextRangeAdjustmentGestureRecognizer. But you cannot find it via gesturesForFailureRequirements. You need to become a delegate of the gestures and then you can find mentioned gesture via the delegate method of shouldRecognizeSimultaneouslyWith otherGestureRecognizer.
Once you add a target + action to that gesture you can observe the dragging and handle edge scrolling.

Pass tap events to superview but handle long press

I'm trying to pass tap events to the superview but handle longpress events. I've added LongPressGestureRecognizer to the top view but the tap events aren't passed to the superview. I tried multiple approaches:
Overriding hitTest doesn't work since the longpress gesture recognizer handler doesn't get called
isUserInteractionEnabled - same as above
Overriding touchesBegan/Ended and calling them manually on the superview doesn't trigger the tap event
Handing complex tap interactions can be hard, and mixing different approaches can make it much much harder.
Generally, the best way to handle it is to have a single view that has multiple gesture recognisers on them. Implement the UIGestureRecognizerDelegate method gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) and gestureRecognizer(_:shouldRequireFailureOf:) to handle conflicts. When a touch event is recognised it can delegate the action to whatever other object needs to deal with it. Having different views all trying to deal with touches at the same time is not a good way to deal with the problem. Gestures are dependent on other gestures and cannot all be handled independently by different views.

Using Swipe Gesture and Touches Began/Moved/Ended at the same time

I'm trying to use a swipe gesture along with some logic in touches began/moved/ended. Ideally, it would be good if:
User swipes left/right, touches began/moved/ended logic is not called (or cancelled).
For all other cases, touches began/moved/ended logic is called as usual.
Is this possible?
I tried adding the following (based on process both touch event and gesture recognizer) but touches moved/ended is still called:
leftSwipeGestureRecognizer.delaysTouchesBegan = true
self.leftSwipeGestureRecognizer.cancelsTouchesInView = false
Should be:
self.leftSwipeGestureRecognizer.cancelsTouchesInView = YES
This mean: touches are cancelled in case gesture was recognized, otherwise, touches began/moved/ended called.
From documentation:
When this property is YES (the default) and the receiver recognizes
its gesture, the touches of that gesture that are pending are not
delivered to the view and previously delivered touches are cancelled
through a touchesCancelled:withEvent: message sent to the view. If a
gesture recognizer doesn’t recognize its gesture or if the value of
this property is NO, the view receives all touches in the multi-touch
sequence.
In this case I would create a custom UIGestureRecognizer for a new behaviour in touches began/moved/ended. Useful link here. Than I would set delegate for both swipe and custome recognizers and implement gestureRecognizer:shouldRequireFailureOfGestureRecognizer: method to fulfill requirements. Link to documentation.

How to get stepper and longpress to coexist?

I tried setting up a view with a longpress gesture and a stepper configured for continuous updates. With the longpress, the continuous feature of the stepper does not occur. For now, I've disabled the longpress. I guess I don't need it. But for future reference, how would I allow for both to coexist?
Just to be clear, here is the way the screen was set up when I tried this.
App was set up with a simple view controller.
A subview was added to this view (could have been a controller, but I just made it a UIView).
Several labels and stepper were added to this subview.
The steppers were wired up as outlets and actions.
A longpress recognizer was added to the main view in IB.
For completeness, a tap gesture was also added to the main view in IB.
Taps on the main view function as expected. Taps on the steppers function as expected. Longpress on the main view functions as expected. Longpress on the stepper does not.
I modified the code called by the longpress to check for the frame of the subview and not act if the touch location was within that rectangle, but that didn't make a difference. I did not try getting the longpress to fail in that situation, but I suppose I'll try that next. EDIT: OK, maybe not. There doesn't seem to be an API for that. However, there is this kludge, that I'm not going to try.
Attached is a screen shot from profiler with an inverted call tree so you can see what each item is being called by.
darkStepped: is the IBAction that is called by the stepper. If the stepper were triggered by a gesture recognizer, wouldn't I expect to see the gesture recognizer in the call tree?
If the stepper were triggered by a gesture recognizer, wouldn't I expect to see the gesture recognizer in the call tree?
The stack trace reveals that the stepper's _updateCount method is dispatched through a timer.
This could be related to the fact that a stepper has an "autoIncrement" mode where, as long as your keep it pressed, it will update at a given (varying) rate. So, instead of simply calling _updateCount, the stepper sets up a timer to handle this behaviour.
For whatever reason the timer is used, the timer explains why you do not see the gesture recogniser in the stack trace.
In your case what happens is that the stepper gets the touches, handles them, and do not forward them to any gesture recognisers attached to it.
This can be explained as follows, although this snippet does not explicitly mention a long press recogniser in relation to a UIStepper control:
According to Apple Docs:
Interacting with Other User Interface Controls
In iOS 6.0 and later, default control actions prevent overlapping gesture recognizer behavior. For example, the default action for a button is a single tap. If you have a single tap gesture recognizer attached to a button’s parent view, and the user taps the button, then the button’s action method receives the touch event instead of the gesture recognizer. This applies only to gesture recognition that overlaps the default action for a control, which includes:
A single finger single tap on a UIButton, UISwitch, UIStepper, UISegmentedControl, and UIPageControl.
...
If you have a custom subclass of one of these controls and you want to change the default action, attach a gesture recognizer directly to the control instead of to the parent view. Then, the gesture recognizer receives the touch event first. As always, be sure to read the iOS Human Interface Guidelines to ensure that your app offers an intuitive user experience, especially when overriding the default behavior of a standard control.
So, it seems you can attach the gesture recogniser directly to the control (possibly you need to subclass UIStepper for this to work, I am not really sure how to interpret the last paragraph). Hopefully this will not disable the basic workings of the stepper (but maybe it will).
After carefully reviewing Apple's docs again, I've found the solution. I added the view controller as the delegate to the longpress gesture recognizer
self.longPress.delegate = self;
(and, of course, adding <UIGestureRecognizerDelegate> to the interface, and then added this method to the view controller:
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
// Determine if the touch is inside the custom subview
if (gestureRecognizer == self.longPress) {
CGPoint touchLocation = [touch locationInView:self.view];
if (CGRectContainsPoint(self.antControl.frame, touchLocation)) {
return NO;
}
}
return YES;
}
This way the gesture recognizer doesn't even get called when the longpress occurs within the frame of self.antControl, which is the subview mentioned in the question.

iOS: Two Gestures, One Target-Action

I'm implementing the Messages app copy-message feature.
You can either double tap or long press on a message to copy it.
How do I do that?
I was thinking of adding two gesture recognizers to the view, one UITapGestureRecognizer (with numberOfTapsRequired set to 2) and one UILongPressGestureRecognizer. They would both have the same target & action.
Then, I think for each of them, I'd call requireGestureRecognizerToFail:, passing the other gesture recognizer.
Is my thinking correct? Is there anything I'm missing, or is there a better way to do this?
Simply add the gestures to your view (easy to do programatically) and set the selector to the desired method. However, you're probably going to get some push back since you don't provide any code or hint that you have tried to solve your problem before coming here. I'm new here as well, but have seen some questions put on hold for those reasons.
As you said that for double tap and long press on a message to copy. So both of them are using for same action. So I think you can do it on same method.
You can try this method in UIGestureRecognizerDelegate
gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:
refer this for more detail:
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIGestureRecognizerDelegate_Protocol/Reference/Reference.html#//apple_ref/occ/intfm/UIGestureRecognizerDelegate/gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:
this helps recognize more than one gesture recognizer at a time.
Yes, as you say, create two gesture recognizers (one long-press and one double-tap) and add them both to the same view.
Don't call requireGestureRecognizerToFail: on either of them because long-press & double-tap gestures play nice together by default.
You can give them both the same target and action, but each gesture requires different logic to determine whether you should show the copy menu.
- (void)messageCopyMenuShowAction:(UILongPressGestureRecognizer *)gestureRecognizer
{
BOOL doubleTap = (gestureRecognizer.numberOfTapsRequired == 2);
if ((doubleTap && gestureRecognizer.state == UIGestureRecognizerStateEnded) || // double-tap
(!doubleTap && gestureRecognizer.state == UIGestureRecognizerStateBegan)) { // long-press
// Show copy menu.
}
}

Resources