Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UIGestureRecognizer subView recognizer problem

The title is hard . The the main case is like this

UIView *superView = [[UIView alloc] initWithFrame:CGRectMake(0,0,400,400)];
UIView *subView = [[UIView alloc] initWithFrame:CGRectMake(-200,-200,400,400)];
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction:)];
[subView addGestureRecognizer:tapGesture];
[superView addSubView:subView];

OK , you will find that the tap gesture will take effect when you click the area in (0,0,200,200) , if you click the point (-150,-150) the tap gesture will not take effect.

I don't know whether the click outside the superView bounds to cause this problem or not.

Anyone have any idea how to fix this?

like image 403
Celery01 Avatar asked Dec 13 '22 13:12

Celery01


2 Answers

To allow subviews lying outside of the superview to respond to touch, override hitTest:withEvent: of the superview.

Documentation on Event Delivery

Touch events. The window object uses hit-testing and the responder chain to find the view to receive the touch event. In hit-testing, a window calls hitTest:withEvent: on the top-most view of the view hierarchy; this method proceeds by recursively calling pointInside:withEvent: on each view in the view hierarchy that returns YES, proceeding down the hierarchy until it finds the subview within whose bounds the touch took place. That view becomes the hit-test view.

  1. Create a subclass of UIView.
  2. Override hitTest:withEvent.
  3. Use this UIView subclass for the superview.

Add method below in subclass:

(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    NSEnumerator *reverseE = [self.subviews reverseObjectEnumerator];
    UIView *iSubView;

    while ((iSubView = [reverseE nextObject])) {

        UIView *viewWasHit = [iSubView hitTest:[self convertPoint:point toView:iSubView] withEvent:event];
        if(viewWasHit) {
            return viewWasHit;
        }

    }
    return [super hitTest:point withEvent:event];
}

Note: Reverse enumerator used since subviews are ordered from back to front and we want to test the front most view first.

like image 111
Min Tsai Avatar answered Jan 10 '23 23:01

Min Tsai


The only workaround I've found for case like that is to create an instance of a view that is transparent for touches as main view. In such case inner view will respond to touches as it fits bounds of main. In the class I've made from different examples found in the net I can control the level of "touch visibility" like so: fully visible - all of the touches end up in the view. only subviews - the view itself invisible, but subviews get their touches. fully invisible - pretty self explanatory I think :)

I didn't try to use it with gesture recognizers, but I don't think there will be any problem, as it works perfectly with regular touches.

The code is simple...

TransparentTouchView.h

#import <UIKit/UIKit.h>

typedef enum{
    TransparencyTypeNone = 0,         //act like usual uiview
    TransparencyTypeContent,          //only content get touches
    TransparencyTypeFull              //fully transparent for touches
}TransparencyType;

@interface TransparentTouchView : UIView {
    TransparencyType      _transparencyType;
}

@property(nonatomic,assign)TransparencyType transparencyType;

@end

TransparentTouchView.m

#import "TransparentTouchView.h"


@implementation TransparentTouchView

@synthesize 
transparencyType = _transparencyType;

- (id)initWithFrame:(CGRect)frame{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
        self.backgroundColor = [UIColor clearColor];
    }
    return self;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    // UIView will be "transparent" for touch events if we return NO
    switch (_transparencyType) {
        case TransparencyTypeContent:
            for(UIView* subview in self.subviews){
                CGPoint p = [subview convertPoint:point fromView:self];
                if([subview pointInside:p withEvent:event]){
                    return YES;
                }
            }
            return NO;
            break;
        case TransparencyTypeFull:
            return NO;
        default:
            break;
    }
    return YES;
}

@end

I believe that you can accomodate it to your needs.

like image 43
Ariel Avatar answered Jan 10 '23 23:01

Ariel