How to steal touches from UIScrollView? - iphone

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)

Related

Interrupting a scrolling/animating UIScrollView with a UIGestureRecognizer

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.

How to get a UIView under a UIScrollView to detect touches?

I have a UIScrollView ontop of my UIViewController recreating an effect like in the Gowalla iPhone app when you're on a spot's page. Under my scroll view I have a button that I want to be able to perform it's action even when the scroll view's frame covers it up (where it's ontop of the button, the scroll view's clear). How would I do something like this? Is it possible? (it has to be, Gowalla [somehow] did it)
As for me, I will transfer touch event to another view by override following methods.
– touchesBegan:withEvent:
– touchesMoved:withEvent:
– touchesEnded:withEvent:
– touchesCancelled:withEvent:
Like this way,
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// pass touch event by default implement.
[super touchesBegan:touches withEvent:event];
// redirect the touch events.
[anotherView touchesBegain:touches withEvent:event];
}
hitTest:withEvent: is used to decide which view should response touch event on the view hierarchy tree. If the view doesn't want to response touch event, it will return nil in hitTest. As result the above touch event methods won't be called.
I have no idea what this "Gowalla" app might do; hopefully, your description of "a button behind the scroll view, that you can touch 'through' the scroll view" is accurate.
If your button behind the scroll view can be sized to fill the entire contentSize area of the scroll view without screwing up your interface, the easiest solution would be to just put it inside the scroll view (under all the other views) and do just that.
Otherwise, your best bet is probably to create a custom view with a clear background to be placed in the scroll view as above. The easy solution is to have the custom view (probably a UIControl) just do whatever touching the button does. If that's not possible for some reason, your best option would be to override hitTest:withEvent: on the custom view to return the underlying button. I'd be wary of overriding hitTest:withEvent: on the scroll view itself, as that might interfere with scrolling.
Try adding the button on top of the scroll view. The only problem with that is if you hit the button, you will not be able to interact with the scrollView, but it will still be visible.

Not receiving touchesEnded/Moved/Cancelled after adding subView

Title more or less says it all. In response to a touchesBegan event, my UIViewController recolours itself and adds some subviews.
It never receives the touchesEnded. I guess because the added subviews are somehow intercepting the event. I tried calling resignFirstResponder on the subviews to no avail.
The code works fine when I don't add the child views and the touch events are called as normal.
Any ideas?
Thanks
EDIT: Bit of detail and how I fixed it.
Basically I had a master view with some subviews, when I touched the subview, the event would be passed through to the master view, however, on this event I was removing the subviews and adding new ones in their place. The fact that the touch originated on a subview which no longer existed meant that the rest of the touch was lost.
I fixed this by overriding hitTest:withEvent in my master view, to stop touches ever getting tested against the subviews
Did you try to set the userInteractionEnabled property to NO for the subview before adding it as a subview ?
You're going to need pass the touch from the subview onto the superview using something like this:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
}

What can be the reason that my UIImageView subclass doesn't receive any touch events?

I have made sure that all superviews of my customized UIImageView and the UIImageView itself have userInteractionEnabled = YES;
Also in the nib all views and subviews have userInteractionEnabled = YES;
But for some reason, these get never called when I click on the UIImageView:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"check!");
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"other check");
}
I have an UIView that acts as an grouping for some subviews. Can that be a problem?
This is just a guess -- is there a UIScrollView somewhere in the view hierarchy? UIScrollViews don't pass touch events like normal views, you have to subclass it and implement a custom touchesBegan method. You can find information about that here -- it's a problem that sent me scratching my head and Googling for several hours the first time I encountered it.
After a while I figured out the following problem:
I had an UIView that I used to group some UIImageViews together, so that I can move all of them at once by moving that UIView only. I made the UIView 0.01 x 0.01 big while it does not clipsToBounds. As I clicked on subviews that were drawing outside the bounds of their superview, the UIView, no touch events where received on these subviews. I don't know if there were any touch events at all.
So, if you have the same problem, make sure that your touches occur on an area which fits into the superview. In my case, I just made that grouping UIView bigger, making the touch-sensible area bigger. After that, it worked. I now slightly remember that Apple had mentioned that in the documentation somewhere, that subviews may draw outside of their superviews, but touch events will be recognized only if they happen in the rectangle of the superview.
It looks like you have a typo there: the correct selector name is 'touchesBegan'. Are you adding this view programatically or through a nib?
touchesBegun should be touchesBegan, but I think that's just a typo?
Depsite that, usually touches not coming through indicates a stressed CPU. Try testing the performance of your app with Instruments.

UIScrollView touches don't seem to work

I used the "View-based Application" template in Xcode for an iPhone project and went into the view controller's XIB.
I changed the view from a basic UIView to a UIScrollView, and now the method
(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
never gets called. I have an NSLog statement in there. It gets called just fine when the controller's view is a UIView, so what's different/special about UIScrollViews that you don't receive touch events? How can I respond to a touch event?
UIScrollView does not pass on all touch events. It does this because it tries to interpret swipe and scroll gestures itself.
Have a look at this thread: http://discussions.apple.com/thread.jspa?messageID=7519817 to learn about UIScrollView and events. The bottom line is that you can handle events in your view's -(void)touchesEnded:(NSSet *)aTouches withEvent:(UIEvent *)anEvent but it's not straight forward.
I have personally seen some funkyness in UIScollView's effect on hit testing sub views.
I don't have any direct experience here, but I was looking over documentation that might explain this yesterday. I think you'll find that UIScrollView captures touches in order to handle its scrolling. Take a look at delaysContentTouches in the documentation window.
Just use a custom content subview, then implement touchesEnded:withEvent: in that view. You'll get all the taps you want!