I have a bunch of UIViews like in the image below. The red/pink (semi-transparent) view is on top of the others.
Red has a UISwipeGestureRecognizer.
Green has as a UITapGestureRecognizer.
Blue has no recognizer.
A tap on the visible (bottom-left) part of Green trigger its recognizer.
A tap on the hidden parts of Green does not trigger its recognizer (Red blocks it).
That's the problem: I want Green to trigger. How can I do this?
In practice, the views may be in any order, any number and be subviews of each others etc. But the problem is the same:
How can I reliably find the uppermost view that can handle the gesture (tap or swipe)?
I tried with the code below. It neatly traverses all views, but it fails since it cannot know if the event is part of a swipe or a tap. So the method always returns the red view. If I remove the swipe-recognizer from Red, the code works correctly.
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *hitView = [super hitTest:point withEvent:event];
if (hitView == self)
{
if (self.hasASwipeRecognizer)
return self; // What if this was a tap?
if (self.hasATapRecognizer)
return self;
else
return nil;
}
else
return hitView;
}
An alternative to adding the gesture recognizer to these views would be to add the gesture recognizers to the parent view and handle the use cases appropriately using the delegate method gestureRecognizer:shouldReceiveTouch: method.
Identify whether the particular recognizer should receive the touch and return YES. For example, if the gesture recognizer passed is a swipe recognizer then check if the touch point is within the green view and return YES. Return NO otherwise.
If there are similar gesture recognizers then I suggest that you keep a reference and verify against it.
Usage
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
CGPoint pointInView = [touch locationInView:gestureRecognizer.view];
if ( [gestureRecognizer isMemberOfClass:[UITapGestureRecognizer class]]
&& CGRectContainsPoint(self.blueView.frame, pointInView) ) {
return YES;
}
if ( [gestureRecognizer isMemberOfClass:[UISwipeGestureRecognizer class]]
&& CGRectContainsPoint(self.greenView.frame, pointInView) ) {
return YES;
}
return NO;
}
One possible solution would be to add a tap gesture recognizer to the top red view and then whenever you get the tap, check whether the tap point intersects with the green view. If so, forward the tap to that view. If not, ignore the tap.
My solutions is:
-(void)handleGesture:(UIGestureRecognizer*)gestureRecognizer {
CGPoint touchPoint = [tapGestureRecognizer locationInView:viewUnderTest];
if ([viewUnderTest pointInside:touchPoint withEvent:nil]) {
NSLog(#"Hit done in view under test");
}
}
Related
I'd like to detect swipe on the entire screen, however, the screen contains UIButtons, and if the user taps one of these buttons, I want the Touch Up Inside event to be triggered.
I've create a UIView on the top of my screen, and added a UIPanGestureRecognizer on it to detect the swipe, but now I need to pass the gesture through that view when I detect that it's a tap rather than a swipe.
I know how to differentiate the gestures, but I've no idea on how to pass it to the view below.
Can anyone help on that? Thanks!
Thanks for your answer. The link helped me to solve part of my problem.
I've set the buttons as subviews of my gestureRecognizer view and I can now start a swipe from one of the buttons (and continue to use the buttons as well). I managed to prevent the buttons to go to the "down" state by using the following code :
UIPanGestureRecognizer *swipe = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(swipeDetected:)];
swipe.maximumNumberOfTouches = 1;
swipe.delaysTouchesBegan =YES;
swipe.cancelsTouchesInView = YES;
[self.gestureRecognitionView addGestureRecognizer:swipe];
there is a BOOL property of UIGestureRecognizer cancelsTouchesInView. default is yes. set it to NO , and the touches will pass thru to the UIView
also have a look at the solution for this question
If you want to prevent the recognizer from receiving the touch at all, UIGestureRecognizerDelegate has a method gestureRecognizer:shouldReceiveTouch: you can use:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
// don't override any other recognizers
if (gestureRecognizer != panRecognizer) {
return YES;
}
CGPoint touchedPoint = [touch locationInView:self.someButton];
return CGRectContainsPoint(self.someButton.bounds, touchedPoint) == NO;
}
I have a view which I'll call parentView which has a subview called childView. Part of childView is outside the bounds of parentView, and childView has a panGestureRecognizer attached to it. I have implemented the following in parentView so that it will recognize touches to childView even though it's outside of its superviews bounds:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (!self.clipsToBounds && !self.hidden && self.alpha > 0)
{
for (UIView *subview in self.subviews)
{
CGPoint subPoint = [subview convertPoint:point fromView:self];
UIView *result = [subview hitTest:subPoint withEvent:event];
if (result != nil)
{
return result;
break;
}
}
}
return [super hitTest:point withEvent:event];
}
Yet when I touch or drag childView, hitTest is not even being called on the parentView. Why is this?
because the event goes down the responder chain and is used before hittest gets called
so the event goes from top to bottom in this case... check out the documentation concerning the responder chain:
it is not very clear though :D
http://developer.apple.com/library/ios/#DOCUMENTATION/EventHandling/Conceptual/EventHandlingiPhoneOS/Introduction/Introduction.html
BUT the important bit:
The hit-test view is given the first opportunity to handle a touch event. If the hit-test view cannot handle an event, the event travels up that view’s chain of responders as described in “The Responder Chain Is Made Up of Responder Objects” until the system finds an object that can handle it.
Touch events. If the hit-test view cannot handle a touch event, the event is passed up a chain of responders that starts with the hit-test view.
I added a UIPanGestureRecognizerto my UIButton. And now my when I drag a short distance with my finger after I pressed the button, the button stops being highlighted. Normally, a UIButton will stay highlighted unless you drag pretty far outside the UIButtons frame. However, with a pan gesture recognizer, now even if I drag a little bit, the button stops being highlighted.
Nothing in my code sets the buttons highlighted property to no. I even tried erasing all the code in my panning gesture recognizers action selector thing(the method that gets called whenever i pan on my button).
I also tried setting the button's highlighted property to NO in the panning gesture recognizer's action selector thing.
This kind of worked its just the highlightedness flashes. When you pan the highlighted goes away then comes back really fast, like a flash. So, this doesnt work too. Any ideas?
Gestures, by default, will cancel the touches in the views that they are linked to. So, when the touches in your button get cancelled it becomes unhighlighted. To prevent this behavior, set the cancelsTouchesInView property of your gesture to NO.
This will work
-(void) panDetected:(UIGestureRecognizer *) gesture
{
if(gesture.state == UIGestureRecognizerStateBegan)
{
yourButton.backgroundImageView.image = [UIImage imageNamed:#"your_button_highlited_image.png"];
}
else if(gesture.state == UIGestureRecognizerStateChanged)
{
}
else if(gesture.state == UIGestureRecognizerStateEnded)
{
yourButton.backgroundImageView.image = [UIImage imageNamed:#"your_button_normal_image.png"];
}
}
I have experienced the same problem. borrrden is right; to fix it, set the viewController the delegate for the gestureRecognizer, and cancel the touch if the touch is on a IUButton. First add the protocol `UIGestureRecognizerDelegate on the .h file. Then, after creating the gesture recognizer, set the view controller as its delegate:
UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(aMethod)];
gestureRecognizer.delegate = self;
[self.view addGestureRecognizer:gestureRecognizer];
[gestureRecognizer release];
Finally override shouldReceiveTouchand cancel unwanted touches:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch{
if ([touch.view isKindOfClass:[UIButton class]]) { //Do not override UIButton touches
return NO;
}
return YES;
}
I have a draggable view that a user will touch, but some rectangles of it will have no image (alpha 0).
When a user clicks the transparent region (I am able to construct the transparent region without the alpha info), I want the view (same class) below the transparent region to detect the touch.
My strategy is to let the view ignore the touch when user touches the transparent area and hope the view below it will automatically catch the touch event. But I'm not sure if this will work. (setting things up to test this will take some time)
Should I take a different approach or the above strategy would work?
Thank you.
Try overwriting the method hitTest:withEvent: in the superview. You can make hitTest:withEvent: return the view you want to handle a given event.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
for (UIView *v in self.subviews){
CGPoint pointInB = [v convertPoint:point fromView:self];
if ([v someConditionYouMayWantToTestFor]){
return v;
}
}
return nil;
}
The method someConditionYouMayWantToTestFor is where you test if you want the subview to capture the event or not.
Basically, here is my view hierarchy (and I appologize if this is hard to read... I'm new here so posting suggestions happily accepted)
--AppControls.xib
-------(UIView)ControlsView
----------------- (UIView)TopBar
----------------- -------------- btn1, btn2, btn3
----------------- UIView)BottomBar
----------------- --------------slider1 btn1, btn2
--PageContent.xib
----------------- (UIView)ContentView
----------------- --------------btn1, btn2, btn3
----------------- --------------(UIImageView)FullPageImage
My situation is that I want to hide and show the controls when tapping anywhere on the PageContent thats not a button and have the controls show, much like the iPhone Video Player. However, when the controls are shown I still want to be able to click the buttons on the PageContent.
I have all of this working, except for the last bit. When the controls are showing the background of the controls receives the touch events instead of the view below. And turning off user interaction on the ControlsView turns it off on all its children.
I have tried overriding HitTest on my ControlsView subclass as follows which I found in a similar post:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView *hitView = nil;
NSArray *subviews = [self subviews];
int subviewCount = [subviews count];
for (int subviewIndex = 0; !hitView && subviewIndex < subviewCount; subviewIndex++){
hitView = [[subviews objectAtIndex:subviewIndex] hitTest:point withEvent:event];
}
return hitView;
}
However, at this point my slider doesn't work, nor do most of the other buttons, and really, things just start getting weird.
So my question is in short: How do I let all the subviews of a view have touch events, while the super view's background is unclickable, and the buttons on views below can receive touch events.
Thanks!
You're close. Don't override -hitTest:withEvent:. By the time that is called, the event dispatcher has already decided that your subtree of the hierarchy owns the event and won't look elsewhere. Instead, override -pointInside:withEvent:, which is called earlier in the event processing pipeline. It's how the system asks "hey view, does ANYONE in your hierarchy respond to an event at this point?". If you say NO, event processing continues below you in the visible stack.
Per the documentation, the default implementation just checks whether the point is in the bounds of the view at all.
Your strategy is to say "yes" when any of your subviews is at that coordinate, but say "no" when the touch would be hitting the background.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
for (UIView * view in [self subviews]) {
if (view.userInteractionEnabled && [view pointInside:[self convertPoint:point toView:view] withEvent:event]) {
return YES;
}
}
return NO;
}
Thanks to #Ben Zutto, Swift 3 solution:
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
for view in self.subviews {
if view.isUserInteractionEnabled, view.point(inside: self.convert(point, to: view), with: event) {
return true
}
}
return false
}
Another approach may be to have an invisible full-screen button behind everything else, and take appropriate action when it is hit.
A slight variant on Ben's answer, dealing w/ children which extend outside their parent.
If clipChildren is YES, then this will not return YES for points which are outside the main control but inside some child.
if clipChildren is NO, this is the same as Ben's.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
BOOL clipChildren = YES;
if (!clipChildren || [super pointInside:point withEvent:event]) {
for (UIView * view in [self subviews]) {
if (view.userInteractionEnabled && [view pointInside:[self convertPoint:point toView:view] withEvent:event]) {
return YES;
}
}
}
return NO;
}