Why does UINavigationBar steal touch events?

后端 未结 12 2275
耶瑟儿~
耶瑟儿~ 2020-11-27 18:48

I have a custom UIButton with UILabel added as subview. Button perform given selector only when I touch it about 15points lower of top bound. And when I tap above that area

相关标签:
12条回答
  • 2020-11-27 19:19

    The following worked for me:

    self.navigationController?.isNavigationBarHidden = true
    
    0 讨论(0)
  • 2020-11-27 19:21

    The solution for me was the following one:

    First: Add in your application (It doesn't matter where you enter this code) an extension for UINavigationBar like so: The following code just dispatch a notification with the point and event when the navigationBar is being tapped.

    extension UINavigationBar {
    open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "tapNavigationBar"), object: nil, userInfo: ["point": point, "event": event as Any])
        return super.hitTest(point, with: event)
        }
    }
    

    Then in your specific view controller you must listen to this notification by adding this line in your viewDidLoad:

    NotificationCenter.default.addObserver(self, selector: #selector(tapNavigationBar), name: NSNotification.Name(rawValue: "tapNavigationBar"), object: nil)
    

    Then you need to create the method tapNavigationBar in your view controller as so:

    func tapNavigationBar(notification: Notification) {
        let pointOpt = notification.userInfo?["point"] as? CGPoint
        let eventOpt = notification.userInfo?["event"] as? UIEvent?
        guard let point = pointOpt, let event = eventOpt else { return }
    
        let convertedPoint = YOUR_VIEW_BEHIND_THE_NAVBAR.convert(point, from: self.navigationController?.navigationBar)
        if YOUR_VIEW_BEHIND_THE_NAVBAR.point(inside: convertedPoint, with: event) {
            //Dispatch whatever you wanted at the first place.
        }
    }
    

    PD: Don't forget to remove the observation in the deinit like so:

    deinit {
        NotificationCenter.default.removeObserver(self)
    }
    

    That's it... That's a little bit 'tricky', but it's a good workaround for not subclassing and getting a notification anytime the navigationBar is being tapped.

    0 讨论(0)
  • 2020-11-27 19:21

    An alternative solution that worked for me, based on the answer provided by Alexandar:

    self.navigationController?.barHideOnTapGestureRecognizer.enabled = false
    

    Instead of overriding the UIWindow, you can just disable the gesture recogniser responsible for the "slop area" on the UINavigationBar.

    0 讨论(0)
  • 2020-11-27 19:22

    Your labels are huge. They start at {0,0} (the left top corner of the button), extend over the entire width of the button and have a height of the entire view. Check your frame data and try again.

    Also, you have the option of using the UIButton property titleLabel. Maybe you are setting the title later and it goes into this label rather than your own UILabel. That would explain why the text (belonging to the button) would work, while the label would be covering the rest of the button (not letting the taps go through).

    titleLabel is a read-only property, but you can customize it just as your own label (except perhaps the frame) including text color, font, shadow, etc.

    0 讨论(0)
  • 2020-11-27 19:23

    Subclass UINavigationBar and add this method. It will cause taps to be passed through unless they are tapping a subview (such as a button).

     -(UIView*) hitTest:(CGPoint)point withEvent:(UIEvent *)event
     {
        UIView *v = [super hitTest:point withEvent:event];
        return v == self? nil: v;
     }
    
    0 讨论(0)
  • 2020-11-27 19:24

    Extending Alexander's solution:

    Step 1. Subclass UIWindow

    @interface ChunyuWindow : UIWindow {
        NSMutableArray * _views;
    
    @private
        UIView *_touchView;
    }
    
    - (void)addViewForTouchPriority:(UIView*)view;
    - (void)removeViewForTouchPriority:(UIView*)view;
    
    @end
    // .m File
    // #import "ChunyuWindow.h"
    
    @implementation ChunyuWindow
    - (void) dealloc {
        TT_RELEASE_SAFELY(_views);
        [super dealloc];
    }
    
    
    - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event {
        if (UIEventSubtypeMotionShake == motion
            && [TTNavigator navigator].supportsShakeToReload) {
            // If you're going to use a custom navigator implementation, you need to ensure that you
            // implement the reload method. If you're inheriting from TTNavigator, then you're fine.
            TTDASSERT([[TTNavigator navigator] respondsToSelector:@selector(reload)]);
            [(TTNavigator*)[TTNavigator navigator] reload];
        }
    }
    
    - (void)addViewForTouchPriority:(UIView*)view {
        if ( !_views ) {
            _views = [[NSMutableArray alloc] init];
        }
        if (![_views containsObject: view]) {
            [_views addObject:view];
        }
    }
    
    - (void)removeViewForTouchPriority:(UIView*)view {
        if ( !_views ) {
            return;
        }
    
        if ([_views containsObject: view]) {
            [_views removeObject:view];
        }
    }
    
    - (void)sendEvent:(UIEvent *)event {
        if ( !_views || _views.count == 0 ) {
            [super sendEvent:event];
            return;
        }
    
        UITouch *touch = [[event allTouches] anyObject];
        switch (touch.phase) {
            case UITouchPhaseBegan: {
                for ( UIView *view in _views ) {
                    if ( CGRectContainsPoint(view.frame, [touch locationInView:[view superview]]) ) {
                        _touchView = view;
                        [_touchView touchesBegan:[event allTouches] withEvent:event];
                        return;
                    }
                }
                break;
            }
            case UITouchPhaseMoved: {
                if ( _touchView ) {
                    [_touchView touchesMoved:[event allTouches] withEvent:event];
                    return;
                }
                break;
            }
            case UITouchPhaseCancelled: {
                if ( _touchView ) {
                    [_touchView touchesCancelled:[event allTouches] withEvent:event];
                    _touchView = nil;
                    return;
                }
                break;
            }
            case UITouchPhaseEnded: {
                if ( _touchView ) {
                    [_touchView touchesEnded:[event allTouches] withEvent:event];
                    _touchView = nil;
                    return;
                }
                break;
            }
    
            default: {
                break;
            }
        }
    
        [super sendEvent:event];
    }
    
    @end
    

    Step 2: Assign ChunyuWindow instance to AppDelegate Instance

    Step 3: Implement touchesEnded:widthEvent: for view with buttons, for example:

    - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
        [super touchesEnded: touches withEvent: event];
    
        UITouch *touch = [touches anyObject];
        CGPoint point = [touch locationInView: _buttonsView]; // a subview contains buttons
        for (UIButton* button in _buttons) {
            if (CGRectContainsPoint(button.frame, point)) {
                [self onTabButtonClicked: button];
                break;
            }
        }    
    }
    

    Step 4: call ChunyuWindow's addViewForTouchPriority when the view we care about appears, and call removeViewForTouchPriority when the view disappears or dealloc, in viewDidAppear/viewDidDisappear/dealloc of ViewControllers, so _touchView in ChunyuWindow is NULL, and it is the same as UIWindow, having no side effects.

    0 讨论(0)
提交回复
热议问题