Interrupting a scrolling/animating UIScrollView with a UIGestureRecognizer - iphone

I have a UIScrollView and a UITapGestureRecognizerwhich are both pretty basic, and have got them to work together in every way I need except one. The UIScrollView doesn't use any zooming, and the UITapGestureRecognizer just writes to the console for now.
I can get the UITapGestureRecognizer to write to the console when the UIScrollView is tapped, either while stationary or animating through setContentOffset, but I cannot get it to work when the UIScrollView is moving due to being swiped. When the UIScrollView is swiped and still in motion, the first tap after swiping will stop it moving, and then only the second tap is picked up by the UITapGestureRecognizer. I hope to get the first tap to both stop the UIScrollView from scrolling and also write to the console through the UITapGestureRecognizer.
I hope that my problem here is just through a gap in my knowledge of either the UIScrollView or UITapGestureRecognizer and there is just a Property to set to fix this, but so far no amount of reading has helped me with this issue. Any ideas on whether this is possible?
Edit: Thanks for the suggestions, please see below
Apologies, I don't think I explained myself very well. I realise the first movement on the stationary UIScrollView is a swipe (which in my case is just handled by the UIScrollView btw, not a gesture recognizer).
The problem is after swiping and releasing, if you tap while it is still in motion (and no other touch is in progress), it isn't picked up by the UITapGestureRecognizer, instead it is only picked up by the UIScrollView and stops the UIScrollView moving. If you let it decelerate then tap, this works fine. Also if it is moving due to an animation but not a swipe, the tap also works fine.
This is the same when using a `UILongPressGestureRecognizer' which is what I want to use ideally, but thought if I can't get it to work with a tap, I have no chance with that!

Sounds like you need to implement the
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
delegate method for your UITapGestureRecognizer and return YES for the UIGestureRecognizers you want to interpret tap gestures in sync with.
e.x.
// Somewhere in your code...
UIGestureRecognizer *tap = [[UITapGestureRecognizer alloc] init];
tap.delegate = self;
[self.view addGestureRecognizer:tap];
[tap release];
// And the delegate method...
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
n.b. You may want to be more discriminating in deciding which UIGestureRecognizer(s) you wish to work in sync with. The example will work with every other gesture recognizer.

The problem is that when the user taps the first time to scroll the UIScrollView, this is not a tap, it is a swipe. It would be considered a tap only when the user taps and releases the finger almost at the same position (without dragging the finger through the screen).
What you could do to solve your problem is: use the UIScrollView delegate method scrollViewWillBeginDragging and then you would know when the user just tapped and will start dragging the scrollview, and also add the UITapGestureRecognizer for those real taps.

Related

iPhone - Stop UISwipeGestureRecognizer from triggering UIPanGestureRecognizer

I have found many posts on managing gestureRecognizers, but I have not been able to find my answer, or rather get anything to work correctly.
I have a UISwipeGestureRecognizer set to down in my Main ViewController, and a PanGestureRecognizer on the x axis attached to an overlay view that pulls the overlay view over to overlay my main view from the right.
Swipe moves down, Pan is for Left to Right.
I want the swipe down to work on all the views, and it does, but when the user swipes down on my overlay view, it also triggers the UIPanGestureRecognizer, which hides the view.
I think this is because there is slight movement in the X direction when you swipe, so the Pan is getting triggered.
How do I get my UIPanGestureRecognizer to only fire if it is a definite Pan to the left or right, and not the trailing end of a swipe?
I have tried shouldRecognizeSimultaneouslyWithGestureRecognizer but it can't really work correctly. Currently they are all set to delegate self.
Any help would tremendously appreciated. Thank you!
The solution was placing
[panGesture requireGestureRecognizerToFail:swipeGesture];
in my viewController's viewDidLoad.
and implementing
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
in my ViewController.m.
I had it placed in my overlay view's viewDidLoad and .m, and that was never getting called. Silly me.

Zoomable UIScrollView with UIButtons capturing touches

I have a UIScrollView and I'm implementing the viewForZoomingInScrollView: delegate method which returns a UIImage which the user can zoom and pan. I've also got some UIButtons as sub views of the UIScrollView which I'm using as annotations, like Google Maps.
The problem I'm having is that a lot of the UIImage can be obscured by the UIButtons when zoomed right out. When trying to pinch to zoom the UIButtons are receiving the touch event instead and the zoom is not happening. You end up having to carefully place your fingers in clear space to zoom.
I note the Google Maps app seems to work ok when there are lots of annotation views, you can still pinch.
I guess I need to priorities the touches, the UIScrollView needs to respond to pinches and pans, while the buttons just taps.
Anyone have experience of this?
I had this exact issue, and it was kind of weird. It wasn't due to any gesture recognizer on the button, it was the UIScrollView's pinch gesture recognizer that was being forwarded to the UIButton for some reason. Also, the UIButton was responding to certain UIGestureRecognizerDelegate calls (like -gestureRecognizerShouldBegin:) but not others (like -gestureRecognizer:shouldReceiveTouch:). It's like the UIScrollView was selectively forwarding some delegate calls to the UIButton and some not.
I finally found a very easy way to solve this issue:
yourScrollView.pinchGestureRecognizer.delaysTouchesBegan = YES;
yourScrollView.pinchGestureRecognizer.delaysTouchesEnded = YES;
Luckily, the default pinch gesture recognizer on UIScrollView's are publicly accessible, and the two delaysTouches property are NO by default. When you set them to YES, if the gesture recognizer could possibly or does recognize a pinch gesture even when it starts on top of a UIButton, it won't forward those touches to the UIButton, and the UIButtons will no longer interfere with the UIScrollView's zooming.
try this
UIGestureRecognizer* tapRecognizer = nil;
for (UIGestureRecognizer* recognizer in yourButton) {
if ( ![recognizer isKindOfClass:[UITapGestureRecognizer class]] ) {
[yourButton removeGestureRecognizer:recognizer];
break;
}
}
so you remove all the gesture recognizers from the button different from UITapGestureRecognizer
take a look at UIView's hitTest:withEvent: method. Inside that method youll need to check for which view you want to return. The view you return will be the one recieving the touches. for example, you can subclass the button and override that method to return the ImageView for your particular scenario.
I fixed this problem in a different way, but this might not be your case.
My buttons were added on an image view, the image view being added by itself to another container view. In the:
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
... method, I returned the image view - bad. Changing to return my very first container view in hierarchy fixed the touches.

UIGestureRecognizers for view in a scroll view

I'm using a few UIGestureRecognizers to pan, rotate and scale a view, which resides inside a scroll view.
My problem is that sometimes the scroll view eats the touches before the gesture recognizers do, so when this happens I end up zooming the scroll view instead of dragging the view. (It doesn't happen all the time. I still can't describe how to reproduce this behavior).
I'm pretty sure this can be solved in some way. For example MPMoviePlayerController doesn't have this problem: in fact, you can put it in a scroll view, and when you pinch it, it works just fine (i.e. it doesn't zoom the outer scroll view too). Does anyone know how does MPMoviePlayerController achieve this?
I've already searched for answers on SO, with no results. Thanks!
Disclaimer: This is just an idea, I haven't tested this.
UIGestureRecognizerDelegate defines gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:. You could try implementing this on your gesture recognizer delegate. This won't prevent the scroll view from zooming. To prevent zooming you could try temporary setting maximumZoomScale minimumZoomScale to zoomScale in the recognizer action method.
At a guess your rotation is conflicting with zoom - both probably require two touches?
In which case try creating a one touch rotation.
Or disable zooming on the scrollview and implement zooming/scaling via pinch gesture recognizer.
see:
http://www.icodeblog.com/2010/10/14/working-with-uigesturerecognizers/
Could you not set scrollEnabled(NO) on your UIScrollView?
http://developer.apple.com/library/ios/#DOCUMENTATION/UIKit/Reference/UIScrollView_Class/Reference/UIScrollView.html
If the value of this property is YES , scrolling is enabled, and if it
is NO , scrolling is disabled. The default is YES.
When scrolling is disabled, the scroll view does not accept touch
events; it forwards them up the responder chain.
Things like zoomToRect:animated: don't say anything about being disabled by this flag; I'd assume that would keep working when you pan/zoom/scroll in response to your gesture events.
I think you can differentiate zoom in/zoom out and scrolling functionality by identify number of tap option on UIScrolView. Here i do same things as you want. you may try it.
- (void)viewDidLoad {
[super viewDidLoad];
imgview=[[UIImageView alloc]initWithImage:[UIImage imageNamed:#"2.png"]];
view1=[[UIView alloc]initWithFrame:CGRectMake(0, 0, 300, 400)];
view1.backgroundColor=[UIColor greenColor];
[view1 addSubview:imgview];
objscrollview.contentMode=UIViewContentModeScaleToFill;
objscrollview.contentSize=CGSizeMake(300, 400);
objscrollview.minimumZoomScale=1;
objscrollview.maximumZoomScale=10;
[objscrollview addSubview:view1];
UITapGestureRecognizer *singleFingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleSingleTap)];
singleFingerTap.numberOfTapsRequired = 1;
[objscrollview addGestureRecognizer:singleFingerTap];
[singleFingerTap release];
}
//Single tap on scrollview call below method
-(void)handleSingleTap
{
NSLog(#"Singletap identify");
}
//While perform zoom in/zoom out action on scroll view it's delegate method call
//In this method, we are return view that want to zoom in/Zoom out..
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView1
{
NSLog(#"hi++++++++++++++++++");
return imgview;
}

touchesBegan method not called when scrolling in UIScrollView

I noticed touchesBegan method is not called in UIScrollView if i immediately place my finger on it and scroll. touchesBegan only gets called after i place my finger for a certain time duration before scrolling. Shouldn't touchesBegan always be called whenever there is a touch on the UIScrollView?
UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(hideKeyBoard:)];
[scrollView addGestureRecognizer:gestureRecognizer];
-(void) hideKeyBoard:(id) sender
{
// Do whatever such as hiding the keyboard
}
I believe that UIScrollView intercepts these events, for the purpose of figuring out if you are going to be scrolling the containing view. Actually, it looks like it gets them first (which is opposite normal processing, where the deepest subview gets them first) so that it can figure out if there is a scroll or pinch gesture. See How does UIScrollView steal touches from its subviews?

How to steal touches from UIScrollView?

Today on my creative time I did some quite comprehensive research on how to steal touches from a UIScrollView and send them instantly to a specific subview, while maintaining the default behavior for the rest of the scroll view. Consider having a UIPickerView inside of a UITableView. The default behavior is that if you drag your finger over the picker view, the scroll view will scroll and the picker view will remain unchanged.
The first thing I tried was to override
- (BOOL)touchesShouldCancelInContentView:(UIView *)view
and simply not allow the UIScrollView to cancel touches inside the picker view. This works, but it has an unpleasant side effect. You would like the picker view to respond immediately and thus you will have to set delaysContentTouches to NO. The problem is that you don't want the rest of the table view to respond immediately, because if it does the table view cell will always get highlighted for a few milliseconds before the scrolling starts.
The second thing I tried was to override
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
because I had read that the scroll view always returns itself, so that it will "steal" the touches from its subviews and later send them to the subview if they weren't of interest to the scroll view. However, this isn't true anymore. UIScrollView's default implementation of hitTest:withEvent: actually returns the subview that should receive the touch. Instead it uses gesture recognizers to intercept the touches.
So the third thing I attempted was to implement my own gesture recognizer and cause it to fail if the touch was outside of the picker view and otherwise succeed. Then I set all the scroll view's gesture recognizers to fail unless my gesture recognizer failed using the following code:
for (UIGestureRecognizer * gestureRecognizer in self.tableView.gestureRecognizers)
{
[gestureRecognizer requireGestureRecognizerToFail:myRecognizer];
}
This does in fact steal the touches from the scroll view, but the picker view never receives them. So I though maybe I could just send all the touches that my gesture recognizer receives using this code:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
for (UITouch *touch in touches)
[touch.view touchesBegan:touches withEvent:event];
}
The above code is a simplified version. I also make sure that the view is a picker view (or one of it's subviews) and set the appropriate state for the gesture recognizer as I mentioned above. I also did the same for canceled, ended and moved. However, the picker view was still not responding.
I also tried one last thing before returning to my regular work. During my extensive googling I read that nested UIScrollViews just magically worked since 3.x, so I tried putting my picker view inside a nested UIScrollView and set the following properties on it:
scrollView.delaysContentTouches = NO;
scrollView.canCancelContentTouches = NO;
As one would expect the outer scroll view didn't treat the inner scroll view any different than it treated the picker view, so the inner scroll view did not receive the touches. I thought that it was a long shot, but it was simple enough to implement, so I thought it was worth to give it a shot.
What I know is that UIScrollView has a gesture recognizer named UIScrollViewDelayedTouchesBeganGestureRecognizer that intercepts the touches and sends them to the appropriate subview after 150 (?) ms. I'm thinking that I should be able to write a similar recognizer that causes the scroll view's default recognizers to fail and instead of delaying the touches immediately sends them to the picker view. So if anyone knows how to write such a recognizer please let me know and if you have any other solution to the problem, you're very welcome share that as well.
Thank you for reading through the whole question and even if you don't know the answer you could still upvote the question so that it gets more attention (hopefully from someone that can answer it). Thanks! :)
Sometimes you have to ask the question before you can find the answer. Dan Ray had a similar problem and solved it with a very different solution.
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
if ([result.superview isKindOfClass:[UIPickerView class]])
{
self.scrollEnabled = NO;
}
else
{
self.scrollEnabled = YES;
}
return result;
}
I've tested the code and it works fine for me as well. However, this is not really stealing touches from the scroll view, so if anyone knows how to actually steal touches that would be great.
Source:
UIPickerView inside UITableView.tableFooterView doesn't receive drag touches
A bit late, but I found this solution:
http://www.cocoanetics.com/2010/06/hacking-uiscrollview-gesture-recognizers/
Works for me
I'm also late to the party, but for newcomers, if you're just looking to flat out ignore swipes on the scroll view, what worked for me was to add a pan gesture recognizer to the view I want ignoring the swipes, like this:
let panGesture = UIPanGestureRecognizer()
panGesture.cancelsTouchesInView = false
myView?.addGestureRecognizer(panGesture)