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
6
  • That's because you're not adding touch events to the subviews. Is it that hard? Commented Jul 17, 2014 at 22:21
  • I cannot use a UIGestureRecognizer because I need ControlEventTouchDown event to be fire. Commented Jul 17, 2014 at 22:24
  • 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.
    – galrito
    Commented Jul 17, 2014 at 22:31
  • And are not handling any touch events on those subviews?
    – galrito
    Commented Jul 17, 2014 at 22:42
  • Not currently. @galrito Commented Jul 17, 2014 at 22:43

5 Answers 5

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.

0
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()
}
1
  • Yes, need to add self.addChildViewController(vcCats);
    – saturngod
    Commented Jun 16, 2017 at 8:39
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
                })
            }
        })
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.

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.