Are UIViewss hit tested for UIGestureRecognizers in the same way as they are for touch events? In particular, is the same hitTest:withEvent: method used?
This would mean a gesture recogniser in a view covered over by a transparent view will not be found, unless hitTest:withEvent: is tweaked to make this happen.
I think Gesture Recognisers are just treated like subviews when it comes to moving up the responder chain so yes they would use the same hit testing method as regular views.
To answer your last question.
Yes, if you have a UIView with a gesture recogniser and overlay it with a transparent view (caveat: it has to have userInteractionEnabled = YES) then the gesture recogniser will not fire.
You can see a nice example of getting around this sort of thing in the WWDC 2014 session 235: "Advanced Scrollviews and Touch Handling Techniques". The transcript might become available here.
Related
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.
I have a super view that has a UITapGestureRecognizer on it. It allows touches within the view because there are clickable items within the view.
When these items are clicked on, I want to take a specific action, not the generic one that covers the entire superview. Unfortunately in my TouchDown event of my child control I don't know how to stop the event here. I know I could create a kludge flag, but this seems like the wrong way to go.
Any advice?
James
OK I got a solution. Totally my problem. I was playing around with trying to get all touches to work and at one point I had set cancelTouchesInView = true on the UITapGestureRecognizer superview. While this didn't stop the other touches from happening, for whatever reason the touches carried through to the superview as well. I understand that this explanation probably makes no sense, but that's what did it. Still trying to wrap my head around how iOS does touch.
My app uses a paged horizontal scroll view. Each page has UIControls which the user can tap.
UIScrollView does a good job of handling cancellation of touches and swipes. If the user starts swiping fast enough, it's always a swipe. If the user touches down long enough to activate the highlighted state, the scroll view doesn't attempt to swipe.
I'm trying to duplicate this behavior with a UIPanGestureRecognizer subclass so that I can respond to downward swipes within my scrollview. However, I can't get the gesture to cancel in the event of the UIControls getting highlighted.
So far I've done the following:
self.refreshGesture.cancelsTouchesInView = YES;
self.refreshGesture.delaysTouchesBegan = NO;
self.refreshGesture.delaysTouchesEnded = NO;
This seems to duplicate the way UIScrollView passes touches to views, but it doesn't duplicate the way that UIScrollView's pan gesture recognizer gets cancelled. self.refreshGesture is always triggered no matter how slowly the user swipes, or what the state of the UIControls are.
I've tried setting the delegate on my gesture, and this may be the way to go. But I haven't found a combination that works. For example, just checking if the touch starts within a UIControl cancels too frequently. I've also tried overriding gestureRecognizerShouldBegin in my controls, but this seems like a hack and has far reaching implications (interferes with UITextView's gestures, for example).
In this GIF, you can see that the control activates on touch, and the scrollview cancels scroll if that happens. But my downward pan gesture is not cancelled in the same manner:
I wasn't able to duplicate this exactly, but there are two possibilities suggested by WWDC 2014 #235.
Add a transparent scrollview over your main content and move its gesture recognizer onto your root view. This is what did. It let me use UIScrollViewDelegate which ended up being sufficient.
Use a "timeout" gesture recognizer. The video suggests requiring the timeout gesture to fail, but in my case it worked better to use a long press gesture and cancel my pan if the long press fired. 0.1 seconds seemed to work better than their suggested 0.15 seconds.
The application I'm building has a full-screen MKMapView, with another UIView subclass placed over it, full-screen as well and completely transparent. I would like for the UIView subclass to handle single touch gestures, such as taps and single finger drags, and ignore anything else. This would allow the MKMapView to be interacted with using other means, especially panning/scrolling with two fingers by disabling 3D functions.
My issue here is that MKMapView does not use the touchesXXX:withEvent: methods for its user interaction. So, I can't detect touch count in those methods on the view and forward to the map. Likewise, the hitTest:withEvent: method can't be used to determine which view handles the touches, because the UIEvent object there returns an empty set of touches.
I've considered letting all touches forward through the view and using a gesture recognizer to handle events, but I really need the single touch/drag on the overlay view to have no effect on the map view.
Is there a way to accomplish this filtering based on the number of touches? Or a way to disable the single touch gestures on the map view?
The solution to this is actually very simple.
Give the map view a parent view that it fills completely
Give the parent view pan and tap gesture recognizers configured to only respond to one finger touches
On the MKMapView, set the scrollEnabled property to NO (the "Allows Scrolling" checkbox in IB)
The gesture recognizers allow you to get the gestures, and setting scrollEnabled to NO prevents the MapView from swallowing the pan gestures.
Sample project here: https://github.com/Linux-cpp-lisp/sample-no-gesture-mapview
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.