How to disable touch input to all views except the top-most view?
Asked Answered
K

12

49

I have a view with multiple subviews. When a user taps a subview, the subview expands in size to cover most of the screen, but some of the other subviews are still visible underneath.

I want my app to ignore touches on the other subviews when one of the subviews is "expanded" like this. Is there a simple way to achieve this? I can write code to handle this, but I was hoping there's a simpler built-in way.

Kesley answered 23/3, 2011 at 11:56 Comment(0)
S
57

Hope this help...

[[yourSuperView subviews]
   makeObjectsPerformSelector:@selector(setUserInteractionEnabled:)
   withObject:[NSNumber numberWithBool:FALSE]];

which will disable userInteraction of a view's immediate subviews..Then give userInteraction to the only view you wanted

yourTouchableView.setUserInteraction = TRUE;

EDIT:

It seems in iOS disabling userInteraction on a parent view doesn't disable userInteraction on its childs.. So the code above (I mean the one with makeObjectsPerformSelector:)will only work to disable userInteraction of a parent's immediate subviews..

See user madewulf's answer which recursively get all subviews and disable user interaction of all of them. Or if you need to disable userInteraction of this view in many places in the project, You can categorize UIView to add that feature.. Something like this will do..

@interface UIView (UserInteractionFeatures)
-(void)setRecursiveUserInteraction:(BOOL)value;
@end

@implementation UIView(UserInteractionFeatures)
-(void)setRecursiveUserInteraction:(BOOL)value{
    self.userInteractionEnabled =   value;
    for (UIView *view in [self subviews]) {
        [view setRecursiveUserInteraction:value];
    }
}
@end

Now you can call

[yourSuperView setRecursiveUserInteraction:NO];

Also user @lxt's suggestion of adding an invisible view on top of all view's is one other way of doing it..

Sorghum answered 23/3, 2011 at 12:4 Comment(15)
I must be doing something wrong. I put this line in my view controller: [[self.view subviews] makeObjectsPerformSelector:@selector(setUserInteractionEnabled:FALSE)]; and I'm getting the compile error "Expected ')' before numeric constant".Kesley
Well, it compiles and runs, but it appears to have no effect - I can still click on any subview. I'm actually getting the touchesBegan event in each view's viewcontroller - is that possibly why this isn't working?Kesley
It is definitely executing, and it is definitely not disabling user interaction on any of my subviews.Kesley
I put the line in my master view's viewDidLoad method, right after adding all the subviews. I am not enabling userInteraction in code anywhere else.Kesley
As I mentioned, each view also has an associated viewcontroller, and in the viewcontroller code I'm responding to the touchesBegan method. Is it possible that your code disables interaction with the view but does not disable interaction with the viewcontroller?Kesley
Weirdly enough, this works fine: for (UIView *v in self.view.subviews) { [v setUserInteractionEnabled:NO]; }Kesley
I just put the above given line in the viewDidLoad of my viewController and I cant touch a thing...Sorghum
That works...that is really weired..I am doing the same thing...Glad something is working for you..Sorghum
I donno if it works or not, But its a very nice piece of coding you have there... accomplishing all that in one line, is really nice... :)Beadsman
Yes @Suicidal makeObjectsPerformSelector is one of the most powerful feature in iOS sdk..Sorghum
Problem is that this won't work for the views that are two level deep or more because [view subviews] gives only the receiver’s immediate subviews.Lost
Yes @Lost but when a parent view's userinteraction is disabled, it's childs user interaction should be disabled too.. right?Sorghum
It's not the case, I tested it and it probably explains why some of the people above are reporting that this solution did not work for them .Lost
Hmmm.. That is strange... I think that is a bug.. What is the point of having userInteraction on a view if its parents user interaction is disabled? Anyway I will edit my answer..Sorghum
This solution works for iOS9.2 simulator. It does not work on iPhone 5 running iOS 8.2. suView.userInteractionEnabled = false; works in both the cases.Sheers
W
47

There are a couple of ways of doing this. You could iterate through all your other subviews and set userInteractionEnabled = NO, but this is less than ideal if you have lots of other views (you would, after all, have to subsequently renable them all).

The way I do this is to create an invisible UIView that's the size of the entire screen that 'blocks' all the touches from going to the other views. Sometimes this is literally invisible, other times I may set it to black with an alpha value of 0.3 or so.

When you expand your main subview to fill the screen you can add this 'blocking' UIView behind it (using insertSubview: belowSubview:). When you minimize your expanded subview you can remove the invisible UIView from your hierarchy.

So not quite built-in, but I think the simplest approach. Not sure if that was what you were thinking of already, hopefully it was of some help.

Wiskind answered 23/3, 2011 at 12:1 Comment(5)
How would you accomplish this though?Attalanta
And you would "block" by adding a gesture recognizer right?Fellini
How do you actually "block" touches? That's the part I don't get.Preview
You can block touches by adding a custom invisible button instead of simple UIView. Check my answer bellow for detailsUnderclassman
+ 1 Disabling userInteraction worked for me both on iOS 8.2 iPhone 5 and iOS9.2 iPhone 6 simulator. The previous solution did not work for iPhone5 8.2.Sheers
L
8

Beware of the code given as solution here by Krishnabhadra:

[[yourSuperView subviews]makeObjectsPerformSelector:@selector(setUserInteractionEnabled:) withObject:[NSNumber numberWithBool:FALSE]];

This will not work in all cases because [yourSuperView subviews] only gives the direct subviews of the superview. To make it work, you will have to iterate recursively on all subviews:

-(void) disableRecursivelyAllSubviews:(UIView *) theView
{
    theView.userInteractionEnabled = NO;
    for(UIView* subview in [theView subviews])
    {
        [self disableRecursivelyAllSubviews:subview];
    }
}

-(void) disableAllSubviewsOf:(UIView *) theView
{
    for(UIView* subview in [theView subviews])
    {
        [self disableRecursivelyAllSubviews:subview];
    }
} 

Now a call to disableAllSubviewsOf will do what you wanted to do.

If you have a deep stack of views, the solution by lxt is probably better.

Lost answered 18/7, 2012 at 17:25 Comment(0)
U
6

I would do this by putting a custom transparent button with the same frame as the superView. And then on top of that button I would put view that should accept user touches. Button will swallow all touches and views behind it wouldn't receive any touch events, but view on top of the button will receive touches normally.

Something like this:

- (void)disableTouchesOnView:(UIView *)view {
    UIButton *ghostButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, view.frame.size.width, view.frame.size.height)];
    [ghostButton setBackgroundColor:[UIColor clearColor]];
    ghostButton.tag = 42; // Any random number. Use #define to avoid putting numbers in code.

    [view addSubview:ghostButton];
}

And a method for enabling the parentView.

- (void)enableTouchesOnView:(UIView *)view {
    [[view viewWithTag:42] removeFromSuperview];
}

So, to disable all views in the parentViev behind yourView, I would do this:

YourView *yourView = [[YourView alloc] initWithCustomInitializer];
// It is important to disable touches on the parent view before adding the top most view.
[self disableTouchesOnView:parentView];
[parentView addSubview:yourView];
Underclassman answered 8/12, 2015 at 19:33 Comment(1)
Thanks, nice and easy way to do it. I also added an alpha = 0.25 to make a nice transparent ghost view. ghostButton.alpha = 0.25;Grandfather
J
5

Add a TapGestureRecognizer to your "background view" (the translucent one which "grays out" your normal interface) and set it to "Cancels Touches In View", without adding an action.

let captureTaps = UITapGestureRecognizer()
captureTaps.cancelsTouchesInView = true
dimmedOverlay?.addGestureRecognizer(captureTaps)
Jaye answered 18/9, 2015 at 9:13 Comment(1)
much better solution that traversing all views to cancel their user interactionIncorrupt
X
5

Just parentView.UserInteractionEnabled = NO will do the work. Parent view will disable user interaction on all the view's subviews. But enable it does not enable all subviews(by default UIImageView is not interactable). So an easy way is find the parent view and use the code above, and there is no need to iterate all subviews to perform a selector.

Xiphoid answered 17/3, 2016 at 17:36 Comment(0)
S
2

I will give my 2 cents to this problem. Iteratively run userInteractionEnabled = false it's one way. Another way will be add a UIView like following.

EZEventEater.h

#import <UIKit/UIKit.h>
@interface EZEventEater : UIView
@end

EZEventEater.m

#import "EZEventEater.h"
@implementation EZEventEater

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


- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
   //EZDEBUG(@"eater touched");
}

- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{

}

- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{

}

In your code you add the EZEventEater view to cover all the views that your may block the touch event. Whenever you want to block the touch event to those views, simply call

eater.userInteractionEnabled = YES;

Hope this helpful.

Spithead answered 31/8, 2014 at 13:58 Comment(1)
Typo in here: self.userInteractionEnabled = true; because if set to false, this view does not accept user interaction; thus passing user interactions to original uiviews.Associate
M
1

In Swift 5, I achieved this behaviour by placing a view right on top(the highlighted one) and setting:

myView.isUserInteractionEnabled = true

This does not let the touches go through it, thus ignoring the taps.

enter image description here

Monolith answered 11/9, 2020 at 5:58 Comment(2)
Didn't work for meFranco
You don't need to explicitly add state isUserInteractionEnabled as it'll be true by default on storyboard, though yeah this solution is correct as if you have an empty view that recognises touch, then it soaks up all the touch events so it won't land under them. Just hide/unhide mask view as needed!Calathus
C
0

For my app, I think it will be sufficient to disable navigation to other tabs of the app (for a limited duration, while I'm doing some processing):

self.tabBarController.view.userInteractionEnabled = NO;

Also, I disabled the current view controller--

self.view.userInteractionEnabled = NO;

(And, by the way, the recursive solutions proposed here had odd effects in my app. The disable seems to work fine, but the re-enable has odd effects-- some of the UI was not renabled).

Chu answered 25/2, 2013 at 4:25 Comment(0)
U
0

Simple solution. Add a dummy gesture that does nothing. Make it reusable by adding it to an extension like this:

extension UIView {
    func addNullGesture() {
        let gesture = UITapGestureRecognizer(target: self,
                                             action: #selector(nullGesture))
        addGestureRecognizer(gesture)
    }
    
    @objc private func nullGesture() {}
}
Unstriped answered 16/8, 2021 at 15:42 Comment(0)
D
-1

setUserInteractionEnabled = NO on the view you want to disable

Dozier answered 23/3, 2011 at 12:1 Comment(1)
This will disable interaction in the subview he is trying to catch touches for.Illegible
P
-1

I had the same problem, but the above solutions did not help. I then noticed that calling super.touchesBegan(...) was the problem. After removing this the event was only handled by the top-most view.

I hope this is of help to anybody.

Patinous answered 2/3, 2017 at 11:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.