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

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.

Related

Detecting when user lifts finger off UIScrollView

I want to detect when the user lifts their finger in a UITableView while they scroll, so in the UIScrollView delegate method scrollViewDidScroll:
func scrollViewDidScroll(scrollView: UIScrollView)
{
// Some code…
if scrollView.panGestureRecognizer.state == .Ended
{
NSLog("ENDED")
}
}
To me this sounds like it should work, so when I investigated further and logged what states happen at this point, the only two logs were Changed and Possible.
To my understanding Ended should happen before possible if the gesture has ended.
Apple docs:
The gesture recognizer has received touches recognized as the end of a continuous gesture. It sends its action message (or messages) at the next cycle of the run loop and resets its state to UIGestureRecognizerStatePossible.
What exactly is going on here? And how do I find when the user lifts their finger off my UITableView during a scroll?
Okay, so scrollViewDidEndDragging:willDecelerate: doesn’t fire when the scroll view isn’t being dragged, it’s actually when the user lifts their finger. This is where Ended is fired.
You can determine when a finger is lifted by implementing the -scrollViewDidEndDragging:willDecelerate: UIScrollViewDelegate method. No need to inspect the state of the underlying gesture recognizer.

iOS: Detecting touch down, segue, touch up

User puts her finger on the screen. This triggers a UITouchEvent, phase Began, which calls the touchesBegan:withEvent: method in controllerA, which performs a segue from controllerA to controllerB.
User lifts her finger off the screen. This triggers a UITouchEvent, phase Ended, which calls some callback method.
Question: What and where is this callback method? It's not in controllerA, and it's not in controllerB. From what I can tell, it's not in any view. But it exists.
To clarify, here's what's going on (according to #switz):
In response to -touchesBegan:withEvent:, a view controller is presented
modally via a segue
When the user lifts up their finger, the view controller should be dismissed.
The question is how to react to the finger being lifted, since
-touchesEnded:withEvent: is not invoked.
The short answer is the presented view controller needs to use the "Over Full
Screen" modalPresentationStyle instead of the default "Full Screen" style
(this can either be specified as the presentation style of the segue, or if
that's "Default" then the presentation style of the presented view controller).
The long answer requires a brief overview of how touch handling works. This
explanation ignores gesture recognizers:
When a touch begins, it's delivered to the "topmost" view that contains the
touch point. From there it gets passed along the responder chain until some
object decides to handle the touch (which is signified by implementing
-touchesBegan:withEvent: and not calling super).
Subsequent changes to the touch (e.g. moved, ended, canceled) are delivered back
to the same view that accepted the touch. The view will continue to receive the
touch events until the touch finishes or cancels.
A touch is canceled either when the application moves to the background (because
e.g. a phone call came in), or when a UIKit class like UIScrollView decides
that it needs to take over touch handling (because the finger moved far enough
that it looks like the user wants to scroll). There's also some funny stuff here
with UIScrollView.delaysContentTouches, but that can be ignored.
But there's a wrinkle, something that isn't documented: touch delivery only
happens so long as the view remains associated with the window. If the view that
is considered "topmost" (the view that is associated with the UITouch) is
removed from the window, then the touch is considered to have vanished and,
importantly, no events for that touch are delivered again, to anyone. This is
true even if the view in question is not the object handling touches.
And that final wrinkle is the cause for this problem. Because the default
"Full Screen" presentation style actually removes the old view controller's
view from the window, the touch handling immediately stops. However, the "Over
Full Screen" presentation style does not remove it, it merely covers up the old
view with the one. "Over Full Screen" is typically used when the presented view
controller is not fully opaque, but in this case we're using it so touch
handling isn't interrupted.
But that's not all. There's another problem here, which is when the view that's
being touched lives inside a UIScrollView (one that either is scrollable or
always bounces). In that case, even with "Over Full Screen", you'll find that,
while the touch events continue to be delivered, moving your finger around a bit
suddenly causes the touch to be canceled. This is because the UIScrollView
doesn't know it's covered up and has decided that the user is actually trying to
scroll. This causes it to cancel the touch.
There is a solution to this, though. It's kind of ugly, but the solution is to
immediately cancel any scrolling on any enclosing scroll view when performing
the segue. This can be done with code like the following:
class ViewController: UIViewController {
// this is called from -touchesBegan:withEvent: from a child view
// the child view is `sender`
func touchDown(sender: UIView) {
var view = sender.superview
while view != nil {
if let scrollView = view as? UIScrollView {
// toggle the panGestureRecognizer enabled state to immediately
// cause it to fail.
let enabled = scrollView.panGestureRecognizer.enabled
scrollView.panGestureRecognizer.enabled = true
scrollView.panGestureRecognizer.enabled = enabled
}
view = view?.superview
}
performSegueWithIdentifier(identifier, sender: self)
}
// ...
}
Of course, no discussion of touch handling would be complete without gesture
recognizers. Gesture recognizers change pretty much everything about touch
handling. They get first dibs on any touches, and they can interrupt view touch
handling at any time. For example, the UIScrollView's UIPanGestureRecognizer
is what is used for scrolling, and when it moves into the "began" state (because
the user has moved their finger enough), that's what causes the touch to be
canceled.
So given this, really the best solution here is to not implement
-touchesBegan:withEvent: at all, but to use a gesture recognizer. The easiest
solution here is to use a UILongPressGestureRecognizer with
minimumPressDuration set to 0 and allowableMovement set to some
ridiculously high value (since you don't want movement to cancel the touch). I'm
recommending this because UILongPressGestureRecognizer is a continuous
recognizer, meaning it will send events for Began, Moved, and Ended, and with
the recommended settings, it will send them in response to the touch beginning,
moving, and ending. What's more, once your recognizer starts handling the touch,
this automatically prevents any other recognizers (such as the scroll view's pan
recognizer) from "taking over" and canceling the touch.
Note that if you're attaching your gesture recognizer to the scrollView itself
(e.g. a UITableView) but only want to respond to touches in certain locations
(such as on a row), then you'll need to restrict the recognizer. You can use the
delegate method gestureRecognizer(_:shouldReceiveTouch:) to do this, something
like this:
func gestureRecognizer(recognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
// if you might be the delegate of multiple recognizers, check for that
// here. This code will assume `recognizer` is the correct recognizer.
// We're also assuming, for the purposes of this code, that we're a
// UITableViewController and want to only capture touches on rows in the
// first section.
let touchLocation = touch.locationInView(self.tableView)
if let indexPath = self.tableView.indexPathForRowAtPoint(touchLocation) {
if indexPath.section == 0 {
// we're on one of the special rows
return true
}
}
return false
}
This way the recognizer won't prevent the tableView's panGestureRecognizer
from scrolling when the user touches elsewhere on the table.

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.

How to differentiate between user swipe and tap action?

I am developing a app in which I have a view which contains subView in it.
I want to track both swipe and tap actions such as a single click.
Actions should be tracked only when the user touches within my subview. When the user taps I want to perform one action, when the user swipes I want perform another.
For tracking the swipe, I implemented UIGestureRecognizer and it is working fine. But I don't know how to track the tap option. Please guide me how to achieve this.
The main thing is, when I tap it should call tap action only and vice versa.
You can use UITapGestureRecognizer for tap gestures.
"UITapGestureRecognizer is a concrete subclass of UIGestureRecognizer
that looks for single or multiple taps. For the gesture to be
recognized, the specified number of fingers must tap the view a
specified number of times."
This method includes the numberOfTapsRequired ("The number of taps for the gesture to be recognized.") and numberOfTouchesRequired ("The number of fingers required to tap for the gesture to be recognized") properties where you can set exactly how you want it to react to user action.
In this case, as you only want it to be activated when tapped once, the default settings for both these properties (both have default values of 1) should be fine.
The best place to get the information is Defining How Gesture Recognizers Interact of Event Handling Guide for iOS
When a view has multiple gesture recognizers attached to it, you may
want to alter how the competing gesture recognizers receive and
analyze touch events. By default, there is no set order for which
gesture recognizers receive a touch first, and for this reason touches
can be passed to gesture recognizers in a different order each time.
You can override this default behavior to:
Specify that one gesture recognizer should analyze a touch before another gesture recognizer.
Allow two gesture recognizers to operate simultaneously.
Prevent a gesture recognizer from analyzing a touch.

iOS UIGestureRecognizer

I have two questions:
Can I implement gesture recogniser that inherits from UISwipeGestureRecognizer and add logic to the UIEvent handlers?
Can I implement UIGestureRecognizer without attaching it to a UIView? Meaning, I will analyze and manage the UIEvent events and call the proper selector (touchesBegan, touchesMoved, touchesEnded, touchesCancelled)?
In the meantime I have problems reseting the gesture recogniser when the state is UIGestureRecognizerStateEnded.
You asked:
Can I implement gesture recogniser that inherits from UISwipeGestureRecognizer and add logic to the UIEvent handlers?
Yes. See Creating a Custom Gesture Recognizer in the Event Handling Guide for iOS. Also see WWDC 2010 session 121 - Advanced Gesture Recognition. It probably depends upon what you want to do, though, and you should see if you can accomplish what you want by configuring the standard swipe gesture's direction and numberOfTouches parameters. I've done more subclassing on continuous gestures like UIPanGestureRecognizer, but I see no reason why you couldn't do it on a swipe, too.
Can I implement UIGestureRecognizer without attaching it to a UIView? Meaning, I will analyze and manage the UIEvent events and call the proper selector (touchesBegan, touchesMoved, touchesEnded, touchesCancelled)?
No. Obviously you can create one, but it just won't receive any of the events until it's added to a UIView and that view receives touches.
In the meantime I have problems reseting the gesture recogniser when the state is UIGestureRecognizerStateEnded.
You'd have to submit a new question providing a relevant code snippet for us to help you on that one. In general, you'd do any post-gesture cleanup when your handler is called for UIGestureRecognizerStateEnded (and UIGestureRecognizerStateCancelled or UIGestureRecognizerStateFailed) and you'd initialize everything for the next gesture when you receive the next UIGestureRecognizerStateBegan.

Resources