Shouldn't UIRecognizers fire when a subview is touched? - ios

Views in my iPad app behave as if they prevent their superview's gesture recognizers from firing when the user initiates such gesture in that view.
Is this expected?
How can I remove that shielding behavior?
What are good practices to debug gesture recognizers?
In more details:
The main "canvas" view of my application, lets the user adds shapes to it with a "long double tap". I attached a gesture recognizer for such gestures to the main view. That works very well: the main view gets called, and reacts by adding a shape to the main view.
Shapes are implemented as subviews of the main view. When the user long-double-taps in the main view, my code instanciate a shape subview, and adds it to the main view. Shape views can be moved around with a long-single-tap recognizer. So I also attach a gesture recognizer for long-single-taps to every shape view. That works very well: the shape view gets called and lets the user move it in the canvas.
However, when the user long-double-taps in a shape view, nothing happens: the shape view is not called, which is expected since it doesn't have a gesture recognizer for long-double-taps. But the main view is not called either. I had thought that since the gesture was not recognized by the shape view, then it would be propagated up in the responder chain to the main view. But this doesn't happen.
My intent is to let the user add overlapping shapes to the main view, so that a long-double-tap on a shape would also add a new shape to the main view.
What could I have missed?
I can of course add a long-double-tap recognizer to shape views, and from there, either forward the gesture to the main view or handle the gesture directly in a way similar to what I do in the main view.
But this sounds wasteful, and more importantly, I'd like to understand the behavior.
Thanks for any suggestion.

It should as far as I can see pass the message along out of the box.
To ensure both gestureRecognizers are not fired you need to do something like:
[longPress requireGestureRecognizerToFail:doubleLongPress];
Update
Just free styling here but if you want to limit the gesture to one view you could try playing with the gesture delegate (this will only respond if the touched view is self.view)
self.myGesture.delegate = self;
In your controller do something like:
//.h
#interface MyController : UIViewController <UIGestureRecognizerDelegate>
// ...
#end
//.m
#implementation MyController
//...
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
{
BOOL shouldReceiveTouch = YES;
if (gestureRecognizer == self.myGesture) {
shouldReceiveTouch = (touch.view == self.view);
}
return shouldReceiveTouch;
}
//...
#end
NB I haven't tested this but I will update when I test it later.

Related

UIPageViewController crashes when holding down a button while it's animating

I have UIPageViewController that animates programatically. The problem is that the view controllers inside it has UIButtons inside them. When I hold down a button and wait until the UIPageViewController animates, the app crashes with the error:
'Failed to determine navigation direction for scroll'
What I think I need to do is to somehow fake that the user releases the button before the UIPageviewController animates.
However, [self.button sendActionsForControlEvents:UIControlEventTouchCancel]; doesn't seem to do the trick. Neither do UIControlEventTouchUpInside.
Is there a better way do to it or am I using sendActionsForControlEvents wrong?
All sendActionsForControlEvents: does is call any methods you've assigned to the control events passed in for the button. It doesn't call any internal methods to programmatically lift up touches or anything like that.
Right before you programmatically animate your page view controller, try using this method to effectively cancel any touches on the pan gesture recognizer of the page view controller's internal scroll view:
- (void)cancelPanGestureTouchesOfPageViewController:(UIPageViewController *)pageVC
{
// Since UIPageViewController doesn't provide any API to access its scroll view,
// we have to find it ourselves by manually looping through its view's subviews.
for (UIScrollView *scrollView in pageVC.view.subviews) {
if ([scrollView isKindOfClass:[UIScrollView class]]) {
// We've found the scroll view, so use this little trick to
// effectively cancel any touches on its pan gesture recognizer
BOOL enabled = scrollView.panGestureRecognizer.enabled;
scrollView.panGestureRecognizer.enabled = !enabled;
scrollView.panGestureRecognizer.enabled = enabled;
}
}
}
(Note that this is messing with the internal view hierarchy of UIPageViewController, so this method is kind of ugly and may break in the future. I generally don't recommend doing stuff like this, but I think in this instance it should be okay.)

UIPanGestureRecognizer top level view not getting events, subviews consuming them

Using UIPanGestureRecognizer & UITapGestureRecognizer on top-level UIView.
The setup in Interface Builder:
ViewController (our main view controller)
UIView (our main view and wired to our UIViewController
Our core UIView has a subview called a “Block” which is simply a UIView.
The Block view has 4 subviews (children) each being an instance of a UIButton.
The UIButton has its Touch Up Inside event wired to the the UIViewController.
The UIView (our main top-level UIView) has a UIPanGestureRecognizer and a UITapGestureRecognizer
Here is the scenario we are trying to accomplish (a.k.a.The behavior):
A user taps a button (a cell).
The button will change its stated from “normal” to “selected”. (This works fine and the code is simple)
With a selected item, a user can place their finger anywhere on the screen an move it up or down
The issue:
Need to know when panning stops.
The top UIView does not receive a gesture stated of ended.
The UIView does not receive a touchesEnded event.
How do you know when the user has lifted their finger? Suppose I start the panning when my finger is over a UIButton, while panning occurs, the UIButton eats the touches begin and end events. Therefore, you have no way of knowing when the user stopped moving their finger across the iPhone/iPad glass.
First implement UIGestureRecognizerDelegate in your view controller.
Then set the delegate on your gesture recognizers to self and implement the following method
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
Your target method should look like this:
-(void)gestureRegognized:(UIGestureRecognizer*)gestureRecognizer
{
if ([gestureRecognizer isMemberOfClass:[UIPanGestureRecognizer class]])
{
//check its state
if(gestureRecognizer.state==UIGestureRecognizerStateBegan)
{
// add your code here
}
else if(gestureRecognizer.state==UIGestureRecognizerStateEnded)
{
// pan gesture ended code goes here
}
}
else if([gestureRecognizer isMemberOfClass:[UITapGestureRecognizer class]])
{
if(gestureRecognizer.state!=UIGestureRecognizerStateFailed)
{
// tap gesture detected
}
}
}

UITapGestureRecognizer on UIView and Its Subview Respond Together When Subview Is Tapped

UITapGestureRecognizer is applied to both UIImageView and its subview (UITextView). However, when I tap on subview, the receiver becomes subview and its parent view (i.e. UIImageView + UITextView). It should however be only subview because that was the one I tapped. I was assuming nested gestures would react first but apparently parent receives the fist tap and then it goes to child.
So, there are different solutions out there for various scenarios (not similar to mine but rather buttons inside scroll view conflict). How can I easily fix my issue without possible subclassing and for iOS 6+ support? I tried delaying touch on start for UIGestureRecognizer on UIImageView and I tried setting cancelsTouchesInView to NO - all with no luck.
Try the following code:
conform the <UIGestureRecognizerDelegate> to your class.
set yourGesture.delegate = self;
then add this delegate Method:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
// return YES (the default) to allow the gesture recognizer to examine the touch object, NO to prevent the gesture recognizer from seeing this touch object.
if([touch.view isKindOfClass: [UITextView class]] == YES)] {
return YES;
}
else {
return NO;
}
}
Hope it will solve your issue. Enjoy Coding..!!!!
That's exactly what is it supposed to do.
View hierarchy is like a tree structure and its traversal during a touch gesture starts from the root node. It is very likely for your parent view to receive gesture first and then its subviews. The traversal skips the nodes for which
userInteractionEnabled = NO.
since, you don't have any code I can't help you to play with this flag. A more general solution is to always set gesture only for your parentView and in the gesture delegates check the coordinates if it belongs to any one of the subview and if yes then call your gesture method for your subview.
Not a clean approach but works. !!
you should implement the UIGestureRecognizer delegate methods and apply the correct policy to the gesture, when multiple gesture are recognized

How can I set up gesture recognizer to interact any UIView when all the views are being animated?

I found the code listed below from https://github.com/DuncanMC/iOS-CAAnimation-group-demo. This particular method allows the user to stop a UIView while it is "in-flight" in a core animation sequence using a gesture recognizer. When the view is tapped, the animation stops. As shown this code will only work on animated view. I have many animated views and I need interaction with any of the views. I think I must set up an array of views (or layers) and cycle through them. Is this correct? How could I do this? Thanks!
/*
This method gets called from a tap gesture recognizer installed on the view myContainerView.
We get the coordinates of the tap from the gesture recognizer and use it to hit-test
myContainerView.layer.presentationLayer to see if the user tapped on the moving image view's
(presentation) layer. The presentation layer's properties are updated as the animation runs, so hit-testing
the presentation layer lets you do tap and/or collision tests on the "in flight" animation.
*/
- (IBAction)testViewTapped:(id)sender
{
CALayer *tappedLayer;
id layerDelegate;
UITapGestureRecognizer *theTapper = (UITapGestureRecognizer *)sender;
CGPoint touchPoint = [theTapper locationInView: myContainerView];
if (animationInFlight)
{
tappedLayer = [myContainerView.layer.presentationLayer hitTest: touchPoint];
layerDelegate = [tappedLayer delegate];
if (((layerDelegate == imageOne && !doingMaskAnimation)) ||
(layerDelegate == waretoLogoLarge && doingMaskAnimation))
{
if (myContainerView.layer.speed == 0)
[self resumeLayer: myContainerView.layer];
else
{
[self pauseLayer: myContainerView.layer];
//Also kill all the pending label changes that we set up using performSelector:withObject:afterDelay
[NSObject cancelPreviousPerformRequestsWithTarget: animationStepLabel];
}
}
}
}
LOL. That demo project is mine. The code is written to find the layer that was tapped, and then use the fact that for a layer that backs a UIView, the layer's delegate is the view itself.
At the point in the code where it finds the layerDelegate, you should make sure it isKindOfClass UIView, then use whatever method is appropriate to match your view with the views you've animated. You could use an IBOutletCollection to keep track of the views that you are animating, or manually create a mutable array and add view objects to it, use view tags, or whatever makes sense for your application.

touches methods not getting called on UIView placed inside a UIScrollView

I have a Custom Scroll View, subclassing UIScrollView. I have added a scroll view in my viewcontroller nib file and changed its class to CustomScrollView. Now, this custom scroll view (made from xib) is added as a subview on self.view.
In this scroll view, I have 3 text fields and 1 UIImageView(named signImageView) added from xib. On clicking UIImageView (added a TapGestureRecogniser), a UIView named signView is added on the custom scroll view. I want to allow User to sign on this view, So I have created a class Signature.m and .h, subclassing UIView and implemented the touches methods (touchesBegan, touchesMoved and touchesEnded) and initialised the signView as follows:
signView = [[Signature alloc]initWithFrame:signImageView.frame];
[customScrollView addSubview:signView];
But when I start signing on the signView, the view gets scrolled and hence the touches methods don't get called.
I have tried adding signView on self.view instead of custom scroll view, but in that case the view remains glued to a fixed position when I start scrolling. (Its frame remains fixed in this case)
Try setting canCancelContentTouches of the scrollView to NO and delaysContentTouches to YES.
EDIT:
I see that similiar question was answered here Drag & sweep with Cocoa on iPhone (the answer is exactly the same).
If the user tap-n-holds the signView (for about 0.3-0.5 seconds) then view's touchesBegan: method gets fired and all events from that moment on go to the signView until touchesEnded: is called.
If user quickly swipes trough the signView then UIScrollView takes over.
Since you already have UIView subclassed with touchesBegan: method implemented maybe you could somehow indicate to user that your app is prepared for him to sign ('green light' equivalent).
You could also use touchesEnded: to turn off this green light.
It might be better if you add signImageView as as subView of signView (instead of to customScrollView) and hide it when touchesBegan: is fired). You would add signView to customScrollview at the same place where you add signImageView in existing code instead.
With this you achieve that there is effectively only one subView on that place (for better touch-passing efficiency. And you could achieve that green light effect by un-hiding signImageView in touchesBegan:/touchesEnded:
If this app-behaviour (0.3-0.5s delay) is unacceptable then you'd also need to subclass UIScrollView. There Vignesh's method of overriding UIScrollView's touchesShouldBegin: could come to the rescue. There you could possibly detect if the touch accoured in signView and pass it to that view immediately.
When ever you add a scrollview in your view hierarchy it swallows all touches.Hence you are not getting the touches began. So to get the touches in your signon view you will have to pass the touches to signon view. This is how you do it.
We achieved this with a UIScrollView subclass that disables the pan gesture recogniser for a list of views that you provide.
class PanGestureSelectiveScrollView: UIScrollView {
var disablePanOnViews: [UIView]?
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let disablePanOnViews = disablePanOnViews else {
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
let touchPoint = gestureRecognizer.location(in: self)
let isTouchingAnyDisablingView = disablePanOnViews.first { $0.frame.contains(touchPoint) } != nil
if gestureRecognizer === panGestureRecognizer && isTouchingAnyDisablingView {
return false
}
return true
}
}

Resources