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

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

Related

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
}
}
}

Cross Directional UIScrollViews - Can I Modify the Scrolling Behaviour?

Here's how the scroll views work: One scroll view is paging enabled in the horizontal direction. Each 'page' of this scroll view contains a vertically scrolling UITableView. Without modification, this works OK, but not perfectly.
The behaviour that's not right: When the user scrolls up and down on the table view, but then wants to flick over to the next page quickly, the horizontal flick/swipe will not work initially - it will not work until the table view is stationary (even if the swipe is very clearly horizontal).
How it should work: If the swipe is clearly horizontal, I'd like the page to change even if the table view is still scrolling/bouncing, as this is what the user will expect too.
How can I change this behaviour - what's the easiest or best way?
NOTE For various reasons, a UIPageViewController as stated in some answers will not work. How can I do this with cross directional UIScrollViews (/one is a table view, but you get the idea)? I've been banging my head against a wall for hours - if you think you can do this then I'll more than happily award a bounty.
According to my understanding of the question, it is only while the tableView is scrolling we want to change the default behaviour. All the other behaviour will be the same.
SubClass UITableView. UITableViews are subClass of UIScrollViews. On the UITableView subClass implement one UIScrollView's UIGestureRecognizer's delegate method
- (BOOL)gestureRecognizer:(UIPanGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UISwipeGestureRecognizer *)otherGestureRecognizer
{
//Edit 1
//return self.isDecelerating;
//return self.isDecelerating | self.bounces; //If we want to simultaneous gesture on bounce and scrolling
//Edit 2
return self.isDecelerating || self.contentOffset.y < 0 || self.contentOffset.y > MAX(0, self.contentSize.height - self.bounds.size.height); // #Jordan edited - we don't need to always enable simultaneous gesture for bounce enabled tableViews
}
As we only want to change the default gesture behaviour while the tableView is decelerating.
Now change all 'UITableView's class to your newly created tableViewSubClass and run the project, swipe should work while tableView is scrolling. :]
But the swipe looks a little too sensitive while tableView is scrolling. Let's make the swipe a little restrictive.
SubClass UIScrollView. On the UIScrollView subclass implement another UIGestureRecognizer's delegate method gestureRecognizerShouldBegin:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
CGPoint velocity = [(UIPanGestureRecognizer *)gestureRecognizer velocityInView:self];
if (abs(velocity.y) * 2 < abs(velocity.x)) {
return YES;
}
}
return NO;
}
We want to make the "swipe is clearly horizontal". Above code only permits gesture begin if the gesture velocity on x axis is double than on y axis. [Feel free to increase the hard coded value "2" if your like. The higher the value the swipe needs to be more horizontal.]
Now change the `UiScrollView' class (which has multiple TableViews) to your ScrollViewSubClass. Run the project. :]
I've made a project on gitHub https://github.com/rishi420/SwipeWhileScroll
Although apple doesn't like this method too much:
Important: You should not embed UIWebView or UITableView objects in UIScrollView objects. If you do so, unexpected behavior can result
because touch events for the two objects can be mixed up and wrongly
handled.
I've found a great way to accomplish this.
This is a complete solution for the problem. In order to scroll the UIScrollView while your UITableView is scrolling you'll need to disable the interaction you have it.
- (void)viewDidLoad
{
[super viewDidLoad];
_myScrollView.contentSize = CGSizeMake(2000, 0);
data = [[NSMutableArray alloc]init];
for(int i=0;i<30;i++)
{
[data addObject:[NSString stringWithFormat:#"%d",i]];
}
UITapGestureRecognizer * tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTap:)];
[self.view addGestureRecognizer:tap];
}
- (void)handleTap:(UITapGestureRecognizer *)recognizer
{
[_myTableView setContentOffset:_myTableView.contentOffset animated:NO];
}
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
{
scrollView.userInteractionEnabled = NO;
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
scrollView.userInteractionEnabled = YES;
}
To sum up the code above, if the UITableView is scrolling, set userInteractionEnabled to NO so the UIScrollView will detect the swipe. If the UITableView is scrolling and the user taps on the screen, userInteractionEnabled will be set to YES.
Instead of using UIScrollView as a container for these multiple table views, try using a UIPageViewController.
You can even integrate this into your existing view controller setup as a child view controller (directly replacing the UIScrollView).
In addition, you'll likely want to implement the required methods from UIPageViewControllerDataSource and possibly one or more of the methods from UIPageViewControllerDelegate.
Did you try the methods : directionalLockEnabled of both your table and scroll and set them up to horizontal for one and vertical for the other ?
Edit :
1)
What you want to do is very complicate since the touch wait some time (like 0.1s) to know what your movement will be. And if your table is moving, it will take your touch immediately whatever it is (because it's suppose to be reactive movement on it).
I don't see any other solution for you but to override touch movement from scratch to detect immediately the kind of mouvement you want (like if the movement will be horizontal) but it will be more than hard to do it good.
2)
Another solution I can advise you is to make your table have left and right margin, where you can touch the parent scroll (pages thing so) and then even if your table is scrolling, if you touch here, only your paging scroll will be touched. It's simpler, but could not fit with your design maybe...
Use UIPageViewController and in the -viewDidLoad method (or any other method what best suits your needs or design) get UIPageViewController's UIScrollView subview and assign a delegate to it. Keep in mind that, its delegate property won't be nil. So optionally, you can assign it to another reference, and then assign your object, which conforms to UIScrollViewDelegate, to it. For example:
id<UIScrollViewDelegate> originalPageScrollViewDelegate = ((UIScrollView *)[pageViewController.view.subviews objectAtIndex:0]).delegate;
[((UIScrollView *)[pageViewController.view.subviews objectAtIndex:0]) setDelegate:self];
So that you can implement UIScrollViewDelegate methods with ease. And your UIPageViewController will call your delegate's -scrollViewDidScroll: method.
By the way, you may be obliged to keep original delegate, and respond to delegate methods with that object. You can see an example implementation in ViewPagerController class on my UI control project here
I faced the same thing recently. My UIScrollview was on paging mode and every page contained a UITableView and like you described it worked but not as you'd expected it to work. This is how solved it.
First I disabled the scrolling of the UIScrollview
Then I added a UISwipeGestureRecognizer to the actual UITableView for left and right swipes.
The action for those swipes were:
[scroll setContentOffset:CGPointMake(currentPointX + 320, PointY) animated:YES];
//Or
[scroll setContentOffset:CGPointMake(currentPointX - 320 , PointY) animated:YES];
This works flawlessly, the only down side is that if the user drags his finger on the UITableVIew that will be considered as a swipe. He won't be able to see half of screen A and half of screen B on the same screen.
You could subclass your scroll view and your table views, and add this gesture recognizer delegate method to each of them...
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:
(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
I can't be sure this is exactly what you are after, but it may come close.

Detect touch event on UIScrollView AND on UIView's components [which is placed inside UIScrollView]

I have UIViewController on my simple storyboard IPad project which contains UIScrollView which is placed over entire surface (1024 x 768). I have created 3 XIB files which are UIViews which my application loads on start in viewDidLoad and add them into UIScrollView. Each of these 3 XIB files contains only one UIButton.
This is hierarchy:
~ UIViewController (UIViewControllerClass is class for this
UIViewController)
~~ UIScrollView (contains 3 identical UIViews)
~~~ UIView (UIViewClass is File's Owner for this XIB file)
~~~~ UIButton
I would like that my UIViewControllerClass becomes aware of both: touch anywhere on UIScrollView component AND if UIScrollView is touched, if UIButton inside of UIView in UIScrollView is touched, to have information that exactly that button is touched.
I made IBAction inside UIViewClass for touch on UIButton inside UIView in UIScrollView and when I set User Interaction Enabled = YES on all elements (UIViewController, UIView and UIScrollView) this method is called as it should.
But at this point my UIViewControllerClass isn't aware that touch occurred inside UIScrollView on that UIButton. I made touch recognizer like this:
UITapGestureRecognizer *touch = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTouch)];
touch.numberOfTouchesRequired = 1;
and added it to UIScrollView component. In this way I am able to detect touch event on UIScrollView component in UIViewControllerClass, but touch event handler for UIButton in UIView which is inside UIScrollView isn't called anymore.
So, I need to have these two informations in UIViewControllerClass:
Touch on UIScrollView component was made
Touch on UIButton in UIView which is inside UIScrollView (if this button was touched) was made
I suppose that attaching touch event recognizer to entire UIScrollView component isn't solution, since it disables all touch event handlers I wrote inside UIViewClass.
I think solution is that somehow touches which are made on components in UIView inside UIScrollView should be sent up to UIViewControllerClass, but I didn't found a way to do this.
If anyone can help me, I'd be very grateful. Thanks in advance.
[edit #1: ANSWER by Zheng]
Tap gesture must have cancelsTouchesInView option set to NO!
For my case above, this line solves everything:
touch.cancelsTouchesInView = NO;
Many thanks to Zheng.
I don't know if this works for you or not, but I've given an answer about touch events for views inside scrollview here:
Dismissing the keyboard in a UIScrollView
The idea is to tell the scrollView not to swallow up all tap gestures within the scroll view area.
I'll paste the code here anyways, hopefully it fixes your problem:
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(hideKeyboard)];
// prevents the scroll view from swallowing up the touch event of child buttons
tapGesture.cancelsTouchesInView = NO;
[pageScrollView addGestureRecognizer:tapGesture];
[tapGesture release];
...
// method to hide keyboard when user taps on a scrollview
-(void)hideKeyboard
{
[myTextFieldInScrollView resignFirstResponder];
}
You can subclass your UIScrollView and override the method - hitTest:withEvent: which is called by the system to determine which view will handle the event. Whenever it is called, you can assume that a touch event occurred inside the scroll view, and by calling the super implementation, you can get the view which would normally process the event.
you can capture any kind of gestures in the UIscrollView. Make sure you also handle some of the default properties as well like set cancelsTouchesInView property to false, it is true by default. Also give some tag nos to your sub views to distinguish in selectors. & also enable their User interaction to true.
let tap = UITapGestureRecognizer(target: self, action:
selector(didTapByUser(_:)))

Shouldn't UIRecognizers fire when a subview is touched?

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.

Detecting swipe gestures on UITableViewCell inside UIScrollView

I am hoping someone will be able to help me with a problem that is doing my head in at the moment!
Given the following view hierarchy
I want to be able to detect swipe gestures on my custom UITableViewCell.
I have subclassed the UIScrollView and have a hitTest:withEvent: method that checks whether I am touching the tableview cell (or its content) or not, in which case I set the following scroll view properties:
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
if ([result.superview isKindOfClass:[UITableViewCell class]] || [result.superview tag] == SUBVIEW_TAG)
{
self.canCancelContentTouches = NO;
self.delaysContentTouches = YES;
} else {
self.canCancelContentTouches = YES;
self.delaysContentTouches = NO;
}
return result;
}
I have also implemented:
- (BOOL)touchesShouldCancelInContentView:(UIView *)view
{
if (view.tag == SUBVIEW_TAG || [[view superview] isKindOfClass:[UITableViewCell class]])
return NO;
return YES;
}
And am returning NO in case the view being touched is the table view cell.
These methods are all getting called and performing their actions as expected, but I am still unable to stop the UIScrollView from "hogging" the swipe gesture.
The interesting thing is that if I include the UIView that contains the tableview and cell on both of the methods above (the one with SUBVIEW_TAG) it works perfectly so I am guessing it must be something to do with the fact that UITableView inherits from UIScrollView.
My main goal is to be able to swipe on the cell to reveal more options for the cell. A horizontal swipe anywhere else on that view would be captured by the scroll view and shift the content horizontally as per its normal behaviour.
Any ideas would be very much appreciated!
Thanks!
Rog
I had a similar problem with a swipe detect for a component inside a scrollview and I was able to resolve it with
[scrollView.panGestureRecognizer requireGestureRecognizerToFail:swipeGesture]
Where scrollView is the scroll view object that acts like container and swipeGesture is the component swipe gesture object inside scrollview.
So, you can define a swipe for the cell object like this (for right swipe in the example, custom it as you want)
UISwipeGestureRecognizer* rightSwipeRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(yourMethod)];
[rightSwipeRecognizer setDirection:UISwipeGestureRecognizerDirectionLeft];
[cell addGestureRecognizer:rightSwipeRecognizer];
and then do
[scrollView.panGestureRecognizer requireGestureRecognizerToFail:rightSwipeRecognizer]
The documentation of requireGestureRecognizerToFail says:
This method creates a relationship with another gesture recognizer
that delays the receiver’s transition out of
UIGestureRecognizerStatePossible. The state that the receiver
transitions to depends on what happens with otherGestureRecognizer:
If otherGestureRecognizer transitions to
UIGestureRecognizerStateFailed, the receiver transitions to its normal
next state.
if otherGestureRecognizer transitions to
UIGestureRecognizerStateRecognized or UIGestureRecognizerStateBegan,
the receiver transitions to UIGestureRecognizerStateFailed.
An example where this method might be called is when you want a
single-tap gesture require that a double-tap gesture fail.
Availability Available in iOS 3.2 and later.
Hope helps!
The solution is pretty simple. All you need to do is add UIScrollView inside you UITableViewCell. It will prevent "hogging" effect during swipe gesture.

Resources