UIButton targets not working if a UIView is added as a subview
Asked Answered
B

5

25

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
Baziotes answered 17/7, 2014 at 22:13 Comment(6)
That's because you're not adding touch events to the subviews. Is it that hard?Dramatization
I cannot use a UIGestureRecognizer because I need ControlEventTouchDown event to be fire.Baziotes
Are the subviews inside of the button's frame or outside? Because if they are outside, they will appear normally but are outside of the button's active area.Philology
And are not handling any touch events on those subviews?Philology
Not currently. @PhilologyBaziotes
Can you show us the code where you create the button and their respective subviews?Philology
P
44

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.

Privy answered 17/7, 2014 at 22:25 Comment(0)
S
3

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()
}
Sardonic answered 11/2, 2017 at 18:21 Comment(1)
Yes, need to add self.addChildViewController(vcCats);Shoal
T
1

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
                })
            }
        })
Timtima answered 27/4, 2020 at 17:56 Comment(0)
S
0

I faced this issue.

In my case, i had nib loaded as subview.

UIViewController *vc = [[[NSClassFromString(@"nib_controller") alloc] initWithNibName:@"nib_name" bundle:nil] autorelease];
[self.view addSubview:vc.view];

Into the nib view (vc.view) all buttons was not being calling the action.

[(UIButton*)[self.view viewWithTag:button_tag] addTarget:self action:@selector(btnActions:) forControlEvents:UIControlEventTouchUpInside];


-(IBAction)btnActions:(id)sender{

    // list star buttons for bookmark
    NSLog(@"btnActions tag %d", [sender tag]);

    // bla bla bla
}

I was able to touch the buttons but the method btnActions was not being fired.

Then, i just tried something really simple and logic.

On the parent view controller, i have an method with same name:

-(IBAction)btnActions:(id)sender{

    // list star buttons for bookmark
    NSLog(@"parent btnActions tag %d", [sender tag]);

    // bla bla bla
}

I just added the NSLog inside to see what happens. The result appeared on log console:

parent btnActions tag xxx

This was not possible in iOS versions previous do 9.

But now, its possible to access methods from parent view if the view is loaded from nib controller view.

For who still confusing, see the bellow illustration

parent -> A
child > B

----------
|        | 
|   A    |
|        |
----------
    |
    |--- -(IBAction)btnActions:(id)sender


----------
|        | 
|   B    |
|        |
----------
    |
    |--- -(IBAction)btnActions:(id)sender (the controller have an method with same name as parent)

The B.view is added from A controller

----------
|        | 
|   A    |
|        |
----------
    |
    |--- (touching some button -> execute this:

UIViewController *B = [[[NSClassFromString(@"B_controller") alloc] initWithNibName:@"B_name" bundle:nil] autorelease];
[self.view addSubview:B.view]; // The B.view is added as subview.

The buttons inside B.view send actions to -(IBAction)btnActions

Sample

[(UIButton*)[self.view viewWithTag:button_tag] addTarget:self action:@selector(btnActions:) forControlEvents:UIControlEventTouchUpInside];

All buttons seems to working well because all them could be touched or clicked (xcode simulator). But the -(IBAction)btnActions method declared into B.controller was not being called. I stucked on solve this for many hours until i realize the actions was being sent to the A.Controller, as described above.

The headache here is, in the iOS early versions this was not possible.

I'm not sure, but i think from iOS9, we have this different behaviour.

The app was working well until iOS8.

Sanctimonious answered 7/10, 2016 at 12:45 Comment(0)
L
0

In my case, there was a .xib file for the buttons below and I added this .xib to my ViewController. @IBActions refused to work due to setting constraints for the .xib file in ViewController.

enter image description here

Lobito answered 2/6, 2020 at 20:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.