UIButton targets not working if a UIView is added as a subview

30,827

Solution 1

Set userInteractionEnabled to NO for your subviews to let touches pass through them and onto your button.

Also make sure to change _verticle.userInteractionEnabled = NO; in your lazy-loader for your horizontal property to _horizontal.userInteractionEnabled = NO; as I believe that's a typo.

Solution 2

please use the code like below (replace the 'ViewControllerFilterCategory' with your own one + the nibName) to add the controller from nib (child one) as the child controller of current controller before adding the view as sub view. Then the iOS will call actions attached to the child controller as you expect:

    let vcCats = ViewControllerFilterCategory(nibName: "ViewControllerFilterCategory", bundle: Bundle.main);
    self.addChildViewController(vcCats);
    self.view.addSubview(vcCats.view);

P.S. don't forget to call 'removeFromParentViewController' before removing your sub view (like below):

@IBAction func onCanceled(_ sender: Any) {
    self.removeFromParentViewController()
    self.view.removeFromSuperview()
}

Solution 3

You have to set isUserInteractionEnabled to true for the view you are trying to add gesture targets.

Also the user interaction is disabled when animation is on as per documentation of Apple UIView.animate function.

"During an animation, user interactions are temporarily disabled for the views being animated. (Prior to iOS 5, user interactions are disabled for the entire application.)"

Check their documentation

So the solution is to run any animation inside DispatchQueue.main.asyncAfter

Also use this call inside completion block of UIView.animate if you are starting another animation from there

For example

UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.7, options: [.curveEaseInOut, .allowUserInteraction], animations: {
                topConstraint.constant = 0
                windowView.layoutIfNeeded()
                oldNotificationView?.removeFromSuperview()
            }, completion: { result in
                DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                    //Its important to run below animation in asyncAfter as otherwise setting delay for 4 seconds seems to change frame such that tap gesture or touchesBegan is not sent for the view. So execting after delay using dispatch queue is right way for touch events to be fired instead of using animate functions delay.
                    UIView.animate(withDuration: 0.5, delay:0
                        , options: [.curveEaseOut , .allowUserInteraction], animations: {
                        topConstraint.constant = -height
                        windowView.layoutIfNeeded()
                    } , completion: { result in
                        notificationView.removeFromSuperview()
                        self.inAppNotificationView = nil
                    })
                }
            })
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.7, options: [.curveEaseInOut, .allowUserInteraction], animations: {
            topConstraint.constant = 0
            windowView.layoutIfNeeded()
            oldNotificationView?.removeFromSuperview()
        }, completion: { result in
            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                //Its important to run below animation in asyncAfter as otherwise setting delay for 4 seconds seems to change frame such that tap gesture or touchesBegan is not sent for the view. So execting after delay using dispatch queue is right way for touch events to be fired instead of using animate functions delay.
                UIView.animate(withDuration: 0.5, delay:0
                    , options: [.curveEaseOut , .allowUserInteraction], animations: {
                    topConstraint.constant = -height
                    windowView.layoutIfNeeded()
                } , completion: { result in
                    notificationView.removeFromSuperview()
                    self.inAppNotificationView = nil
                })
            }
        })
Share:
30,827
Jason Silberman
Author by

Jason Silberman

Updated on July 09, 2022

Comments

  • Jason Silberman
    Jason Silberman almost 2 years

    I have a UIButton. It has two subviews. I then call:

    [createButton addTarget:self action:@selector(openComposer) forControlEvents:UIControlEventTouchUpInside];
    

    If I tap the parts of the UIButton that aren't covered but one of its subviews, it works fine. However if I tap one of the subviews instead, it does not trigger the action.

    On both subviews, I has .userInteractionEnabled set to YES.

    Both subviews are plain UIViewss with a frame and backgroundColor.

    How can I get the tap to "pass through" the UIView and onto the UIButton?

    Thanks

    EDIT: I need to use UIControlEvents because I need UIControlEventTouchDown.

    EDIT 2:

    Here is the code for the button.

    @interface CreateButton ()
    
    @property (nonatomic) Theme *theme;
    @property (nonatomic) UIView *verticle;
    @property (nonatomic) UIView *horizontal;
    @property (nonatomic) BOOL mini;
    
    @end
    
    @implementation CreateButton
    
    #pragma mark - Accessors
    
    - (UIView *)verticle {
        if (! _verticle) {
            _verticle = [[UIView alloc] init];
            _verticle.backgroundColor = [self.theme colorForKey:@"createButtonPlusBackgroundColor"];
            _verticle.userInteractionEnabled = NO;
        }
        return _verticle;
    }
    
    - (UIView *)horizontal {
        if (! _horizontal) {
            _horizontal = [[UIView alloc] init];
            _horizontal.backgroundColor = [self.theme colorForKey:@"createButtonPlusBackgroundColor"];
            _verticle.userInteractionEnabled = NO;
        }
        return _horizontal;
    }
    
    #pragma mark - Init
    
    - (instancetype)initWithTheme:(Theme *)theme frame:(CGRect)frame {
        self = [super initWithFrame:frame];
        if (self) {
            _theme = theme;
    
            self.layer.cornerRadius = roundf(frame.size.width / 2);
            self.layer.shadowColor = [UIColor blackColor].CGColor;
            self.layer.shadowOpacity = .1f;
            self.layer.shadowOffset = CGSizeZero;
            self.layer.shadowRadius = 15.f;
            self.backgroundColor = [self.theme colorForKey:@"createButtonBackgroundColor"];
    
            [self addSubview:self.verticle];
            [self addSubview:self.horizontal];
    
            [self addTarget:self action:@selector(animate) forControlEvents:UIControlEventTouchDown];
            [self addTarget:self action:@selector(animate) forControlEvents:UIControlEventTouchUpInside];
            [self addTarget:self action:@selector(animate) forControlEvents:UIControlEventTouchUpOutside];
        }
        return self;
    }
    
    #pragma mark - Actions
    
    - (void)animate {
        [UIView animateWithDuration:0.2 delay:0 usingSpringWithDamping:0.4 initialSpringVelocity:8 options:kNilOptions animations:^{
            if (self.mini) {
                self.transform = CGAffineTransformMakeScale(1.0, 1.0);
                self.mini = NO;
            } else {
                self.transform = CGAffineTransformMakeScale(0.90, 0.90);
                self.mini = YES;
            }
        } completion:nil];
    }
    
    #pragma mark - UIView
    
    - (void)layoutSubviews {
        CGSize size = self.bounds.size;
    
        NSInteger width = 3;
        NSInteger verticleInset = 12;
        self.verticle.frame = CGRectMake((size.width - width) / 2, verticleInset, width, size.height - (verticleInset * 2));
        self.horizontal.frame = CGRectMake(verticleInset, (size.height - width) / 2, size.width - (verticleInset * 2), width);
    }
    
    @end