Allow Uiscrollview and Its Subviews to Both Respond to a Touch

Allow UIScrollView and its subviews to both respond to a touch

First, a disclaimer. If you set userInteractionEnabled to NO on the UIScrollView, no touch events will be passed to the subviews. So far as I'm aware, there's no way around that with one exception: intercept touch events on the superview of the UIScrollView, and specifically pass those events to the subviews of UIScrollView. To be honest, though, I don't know why you would want to do this. If you're wanting to disable specific UIScrollView functionality (like...well, scrolling) you can do that easily enough without disabling UserInteraction.

If I understand your question, you need tap events to be processed by the UIScrollView and passed to the subviews? In any case (whatever the gesture is), I think what you're looking for is the protocol method gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: in the protocol UIGestureRecognizerDelegate. In your subviews, whatever gesture recognizers you have, set a delegate (probably whatever class is setting the UIGestureReconginzer in the first place) on the gesture recognizer. Override the above method and return YES. Now, this gesture will be recognized along with any other recognizers that might have 'stolen' the gesture (in your case, a tap). Using this method you can even fine tune your code to only send certain kinds of gestures to the subviews or send the gesture only in certain situations. It gives you a lot of control. Just be sure to read about the method, especially this part:

This method is called when recognition of a gesture by
either gestureRecognizer or otherGestureRecognizer would block the
other gesture recognizer from recognizing its gesture. Note that
returning YES is guaranteed to allow simultaneous recognition;
returning NO, on the other hand, is not guaranteed to prevent
simultaneous recognition because the other gesture recognizer's
delegate may return YES.

Of course, there's a caveat: This only applies to gesture recognizers. So you may still have problems if you're trying to use touchesBegan:, touchesEnded, etc to process the touches. You can, of course, use hitTest: to send raw touch events on to the subviews, but why? Why process the events using those methods in UIView, when you can attach a UIGestureRecognizer to a view and get all of that functionality for free? If you need touches processed in a way that no standard UIGestureRecognizer can provide, subclass UIGestureRecognizer and process the touches there. That way you get all the the functionality of a UIGestureRecognizer along with your own custom touch processing. I really think Apple intended for UIGestureRecognizer to replace most (if not all) of the custom touch processing code that developers use on UIView. It allows for code-reuse and it's a lot easier to deal with when mitigating what code processes what touch event.

UIScrollView sending touches to subviews

Note: Thank you all for your contributions, specially to Aaron Hayman.

I was able to figure it out by doing the following on the UIScrollView sub-class I had:

-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
CGPoint pointOfContact = [gestureRecognizer locationInView:self];

// The view with a tag of 200 is my A view.
return (![[self hitTest:pointOfContact withEvent:nil] isEqual:[self viewWithTag:200]]);
}

How to send the touch events of UIScrollView to the view behind it?

First off UIScrollViews only inherently recognize UIPanGestureRecognizers and UIPinchGestureRecognizers so you need to add a UITapGestureRecognizer to the UIScrollView so it can recognize any tapping gestures as well:

UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];

// To prevent the pan gesture of the UIScrollView from swallowing up the
// touch event
tap.cancelsTouchesInView = NO;

[scrollView addGestureRecognizer:tap];

Then once you receive that tap gesture and the handleTap: action is triggered, you can use locationInView: to detect whether the tap gesture's position is in fact within the frame of one of the images below your scroll view, for example:

- (void)handleTap:(UITapGestureRecognizer *)recognizer {

// First get the tap gesture recognizers's location in the entire
// view's window
CGPoint tapPoint = [recognizer locationInView:self.view];

// Then see if it falls within one of your below images' frames
for (UIImageView* image in relevantImages) {

// If the image's coordinate system isn't already equivalent to
// self.view, convert it so it has the same coordinate system
// as the tap.
CGRect imageFrameInSuperview = [image.superview convertRect:image toView:self.view]

// If the tap in fact lies inside the image bounds,
// perform the appropriate action.
if (CGRectContainsPoint(imageFrameInSuperview, tapPoint)) {
// Perhaps call a method here to react to the image tap
[self reactToImageTap:image];
break;
}
}
}

This way, the above code is only performed if a tap gesture is recognized, your program only reacts to a tap on the scroll view if the tap location falls within an image; otherwise, you can just scroll your UIScrollView as usual.

UIScrollView with touch interceptor for contentSize to control subviews

You're close with placing a transparent view on top of the others. But, you need to override one more method.

You want to override hitTest:withEvent on the transparent view. This method is used while traversing the responder chain to see which view handles a touch in a particular area. If a view handles that touch, it returns Itself. If it wants to pass it on to the next view below it, it returns nil. If it knows another view handles that touch, it can return that view.

So, in your case, if the point is in the target area of your top transparent view, you return view1. View1's gesture recognizer should then be called.

Example:

InterceptorView is a transparent view which lies on top of the scrollView. TargetView is a view inside the scrollView, and has a TapGestureRecognizer attached to it.

class InterceptorView: UIView {

required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}

@IBOutlet weak var targetView1: UIView?

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
print("[Interceptor] Testing point: \(point) ")
if self.pointInside(point, withEvent: event) {
println("Hit")
return targetView1
}
else {
println()
return nil;
}
}
}

--

class TargetView: UIView {

required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}

@IBAction func handleGesture(gestureRecognizer: UIGestureRecognizer) {
let location = gestureRecognizer.locationInView(self)
print("[TargetView] Got gesture. Location \(location) ")
if (pointInside(location, withEvent: nil)) {
println("Inside");
}
else {
println("Outside");
}
}
}

Here's the project:
https://github.com/annabd351/GestureForwarding

(there's some other stuff in there too, but it works!)

How does UIScrollView steal touches from its subviews?

You can implement hitTest:withEvent: method in your UIView subclass.

This method gets called to check what subview must receive the touch event, so you can perform some action there before it actually happens. You can also change subview that must receive event.

Subviews of UIScrollView not responding to touch following animation of Scroll View's height

As per OP comments...

The likely issue was due to explicitly changing the frame(s) of the view elements, which were set up with auto-layout constraints. Common issue, and can cause all sorts of odd errors / conditions.

Changing the animated view collapse / expand methods to changing constraint properties instead of explicit frame changes fixed the problem.



Related Topics



Leave a reply



Submit