What's the best way to passthrough a touch event to all subviews?
ViewController -> view -> (subview1, subview2)
I would like subview1 & subview2 to both respond to the touch event.
Set the tags on the subviews equal to noticable tags. Then do a tree search through the views looking for those tags. Unfortunately there is not really a nice way to do this without subclassing. If you were willing to subclass then you would subclass view and then upon touch throw a NSNotification that all other views of that same subclass would listen for.
In the parent's touch handler, you can iterate over the subviews of that view and call the same handler:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
for(UIView *subview in [self.view subviews]) {
[subview touchesBegan:touches withEvent:event];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
for(UIView *subview in [self.view subviews]) {
[subview touchesMoved:touches withEvent:event];
}
}
Or if you needed to identify specific subviews, you could assign integer Tags to the subviews to identify them later:
- (void)loadView {
UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(10,10,10,10)];
view1.tag = 100;
[self.view addSubview:view1];
UIView *view2 = [[UIView alloc] initWithFrame:CGRectMake(20,20,20,20)];
view2.tag = 200;
[self.view addSubview:view2];
}
Then later in the ViewController method invoked by the touch event
- (void)touchEventResponder {
UIView *view1 = [self.view viewWithTag:100];
// Do work with view1
UIView *view2 = [self.view viewWithTag:200];
// Do work with view2
}
Related
I'm trying to add touch events to small view blocks drifting from screen right side to left side. The problem is layer.presentationLayer hitTest:point method returns nothing, even if I do actually tap within the bounds of the view block.
Finally, I find a workaround. But, two questions still confuse me.
The converted point does not seem to be right, how to properly use presentationLayer hitTest?
In my code snippet, UIViewAnimationOptionAllowUserInteraction is not set, why I can handle touch event anyway?
The following are the code, any help will be appreciated
viewDidLoad
CGRect bounds = [[UIScreen mainScreen] bounds];
for (int i = 0; i < 15; i++) {
ViewBlock *view = [[ViewBlock alloc] initWithFrame:CGRectMake(bounds.size.width, 200, 40, 40)];
[self.view addSubview:view];
[UIView animateWithDuration:20 delay:i * 21 options:UIViewAnimationOptionCurveLinear animations:^{
view.frame = CGRectMake(-40, 200, 40, 40);
} completion:^(BOOL finished) {
[view removeFromSuperview];
}];
}
ViewBlock.m
#implementation ViewBlock
- (instancetype)initWithFrame:(CGRect)frame{
if (self = [super initWithFrame:frame]) {
self.backgroundColor = [UIColor redColor];
}
return self;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
CALayer *presentationLayer = self.layer.presentationLayer; //Do NOT modify presentationLayer
if (presentationLayer) {
CGPoint layer_point = [presentationLayer convertPoint:point fromLayer:self.layer.modelLayer];
if ([presentationLayer hitTest:point]) {
return self; //not worked
}
if ([presentationLayer hitTest:layer_point]) {
return self; //not worked either
}
if (CGRectContainsPoint(presentationLayer.bounds, layer_point)) {
return self; //the workaround
}
}
return [super hitTest:point withEvent:event];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[super touchesBegan:touches withEvent:event];
NSLog(#"touches began");
}
- (void)didMoveToSuperview{
[super didMoveToSuperview];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapTouched:)]
;
tap.cancelsTouchesInView = NO;
[self addGestureRecognizer:tap];
}
- (void)tapTouched:(UITapGestureRecognizer *)sender{
NSLog(#"touches gesture");
}
#end
Q1: The converted point does not seem to be right, how to properly use presentationLayer hitTest?
What you are missing is the parameter of hitTest: should be in the coordinate system of the receiver's super layer, so the code should be:
CGPoint superLayerP = [presentationLayer.superlayer convertPoint:layer_point fromLayer:presentationLayer];
if ([presentationLayer hitTest:superLayerP]) {
return self;
}
Q2:In my code snippet, UIViewAnimationOptionAllowUserInteraction is not set, why I can handle touch event anyway
I think you can get touch event because you override - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event and return self as result, so the event will pass to it. By default(If you don't override the hitTest:withEvent: method), you won't get touch event.
If you set UIViewAnimationOptionAllowUserInteraction option, you can get touch event on the view's final frame (CGRectMake(-40, 200, 40, 40)), but in this example, the final frame is off the screen, you can set it to CGRectMake(0, 200, 40, 40) and have a test.
I want to hide the keyboard by touching the view. Everybody recommends to use this method, saying there's no need to link or anything else, but is not working.
The problem is that my method is not called.Is there anything else that should be done?
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[[self view] endEditing:YES];
}
I had trouble with this so use a method that loops through all views seeing if they are textviews and firstResponders. Not sure of the structure of a UITextView but you might need to check for that too although it may be covered.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
for (UIView *txt in self.view.subviews){
if ([txt isKindOfClass:[UITextField class]] && [txt isFirstResponder]) {
[txt resignFirstResponder];
}
}
}
The best approach is to create a "lock view" which is a UIView that takes over the whole screen once the textField becomesFirstResponder. Make sure it's on top of all views (well, besides the textview, of course).
- (void)loadLockView {
CGRect bounds = [UIScreen mainScreen].bounds;
_lockView = [[UIView alloc] initWithFrame:bounds];
_lockView.backgroundColor = [UIColor clearColor];
UITapGestureRecognizer *tgr = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(lockViewTapped:)];
[_lockView addGestureRecognizer:tgr];
[self.view addSubview:_lockView];
}
- (void)lockViewTapped:(UITapGestureRecognizer *)tgr {
[_lockView removeFromSuperView];
[_textField resignFirstResponder];
}
Use UITapGestureRecognizer For Dismiss keyboard.
Write this code on your viewdidload().
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]
initWithTarget:self
action:#selector(dismissKeyboard)];[self.view addGestureRecognizer:tap];
and in dismissKeyboard method put this code.
-(void)dismissKeyboard
{
[TextFieldName resignFirstResponder];
}
There seems to be an answer, but when I try the method provided, it just doesn't work!
#interface MyView : UIView
#end
#implementation MyView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *hitView = [super hitTest:point withEvent:event];
NSLog(#"hitView:%#", hitView);
if (hitView == self) {
return nil;
}
return hitView;
}
- (void)testUserInteraction
{
UIViewController *vc = [[UIViewController alloc] init];
self.window.rootViewController = vc;
MyView *myView = [[MyView alloc] initWithFrame:CGRectMake(0, 0, 320, 568)];
myView.userInteractionEnabled = NO;
[vc.view addSubview:myView];
UIView *subView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 50, 50)];
subView.backgroundColor = [UIColor orangeColor];
[myView addSubview:subView];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapped)];
[subView addGestureRecognizer:tap];
}
When myView.userInteractionEnabled = YES; everything works fine. but when myView.userInteractionEnabled = NO; wherever I tapped the screen, it just output hitView:(null)
So is this method no longer working, or did I miss something?
yes, because you disabled user interaction.
When you call super-hittest,the return from super-hittest depends on userInteraction property.
If it is enabled then only, it returns either itself or some subView.
If not enabled, it will always return nil.
If I understand correctly, you want to know when a touch happens inside your custom UIView, while user interaction on the view is disabled. If this is so, throw the following inside the - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event method. This condition should read only touch events inside your custom view
if ([self pointInside:point withEvent:event]) {
NSLog(#"TOUCH IS INSIDE THE VIEW");
}
I have a very complex app with lots of stacked views which contain lots of buttons ,drawing areas and other custom touch handling . I am displaying a draggable helper view which can be on top of all the other views . I need to dismiss this view if the user taps anywhere outside the helper view . I have tried using multiple UIWindows and adding Gesture recognizers to the UIWindow .
A easy is to add a transparent button which bounds is equal the superview's bounds. And the superview insert the transparent button below your helper view.
The transparent button add a click event which can dismiss the helper view and the transparent button it self.
For example :
UIButton *transparencyButton = [[UIButton alloc] initWithFrame:superview.bounds];
transparencyButton.backgroundColor = [UIColor clearColor];
[superview insertSubview:transparencyButton belowSubview:helperView];
[transparencyButton addTarget:self action:#selector(dismissHelper:) forControlEvents:UIControlEventTouchUpInside];
and the dismissHelper: method can do this :
- (void)dismissHelper:(UIButton *)sender
{
[helperView dismiss];
sender.hidden = YES;
// or [sender removeFromSuperview]
}
You can check the view being touched by going through the touches and looking at the .view property of the touch. It reflects the view from where the view actually originates.
Assuming that you keep a reference to your view (either via an IBOutlet or otherwise) to your view called "myView" the below works.
In your .m file. The touchesBegan: function is triggered every time the user touches a finger down. We loop through the touches to see if the originating view of the touch is NOT equal to "myView". This comparison could also be done via checking the class, tag or any other property you use to identify your view. Examples given below.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"touches began");
UITouch *touch = [touches anyObject];
if(touch.view!=myView){
myView.hidden = YES;
}
}
Or in case of using a tag:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"touches began");
UITouch *touch = [touches anyObject];
if(touch.view.tag!=23){
myView.hidden = YES;
}
}
create an hittestintercept view. this can make the view below handle events like before.
#protocol HitTestInterceptViewDelegate <NSObject>
- (BOOL)interceptHitTest:(CGPoint)point withEvent:(UIEvent *)event;
#end
#interface HitTestInterceptView : UIView
#property(nonatomic, weak) id<HitTestInterceptViewDelegate> hitTestInterceptDelegate;
#end
HtiTestInterceptView.m
#import "HitTestInterceptView.h"
#implementation HitTestInterceptView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if ([_hitTestInterceptDelegate interceptHitTest:point withEvent:event] == NO) {
return [super hitTest:point withEvent:event];
}
return nil;
}
#end
then use it in the toast view
- (void)dismissIfTouchOutsideInView:(UIView *)view {
if (_interceptView != nil) return;
_interceptView = [[HitTestInterceptView alloc] initWithFrame:view.bounds];
_interceptView.backgroundColor = [UIColor clearColor];
_interceptView.hitTestInterceptDelegate = self;
[view insertSubview:_interceptView belowSubview:self];
}
- (BOOL)interceptHitTest:(CGPoint)point withEvent:(UIEvent *)event {
dispatch_async(dispatch_get_main_queue(), ^{
// [self dismiss];
});
return YES;
}
I put UIButton inside UITableViewCell in UITableView that is behind UIScrollView. I subclassed UIScrollView to forward touches to UITableView.
So method from UITableViewDelegate didSelectRow is calling properly. The problem is that UIButton inside table cell is not receiving TouchUpInside actions.
How can I solve this problem without deleting ScrollView over TableView?
EDIT:
I resolved this issue by detecting which view will receive touch. Here's the code:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UIView *hitView = [self hitTest:[(UITouch*)[[touches allObjects] objectAtIndex:0] locationInView:self] withEvent:event];
if ([hitView isKindOfClass:[UIButton class]]) {
[(UIButton*)hitView sendActionsForControlEvents:UIControlEventTouchUpInside];
}
[super touchesBegan:touches withEvent:event];
}
If you want to enable actions for bouth objects - UIScrollView and UIButton you should to implement custom hit test mechanism for ScrollView.
In your UIScrollView subclass override - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event method to make views behind ScrollView available for getting events.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
return __touchesEnabled;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
_touchesEnabled = NO;
UIWindow *window = [UIApplication sharedApplication].delegate.window;
for(UITouch *touch in touches) {
CGPoint point = [touch locationInView:self];
point = [window convertPoint:point fromView:self];
UIView *view = [window hitTest:point withEvent:event];
[view touchesBegan:touches withEvent:event];
}
_touchesEnabled = YES;
}
It works for me
Since you have added your scroll view over the UIButton, all the touch actions will not be passed to the button.
[yourScrollView setUserInteractionEnabled:NO];
This may solve your problem.