How to prevent UINavigationBar from blocking touches on view

Subclass UINavigationBar and override- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event such that it returns NO for the rect where the view you want to receive touches is and YES otherwise.

For example:

UINavigationBar subclass .h:

@property (nonatomic, strong) NSMutableArray *viewsToIgnoreTouchesFor; 

UINavigationBar subclass .m:

- (NSMutableArray *)viewsToIgnoreTouchesFor
{
    if (!_viewsToIgnoreTouchesFor) {
        _viewsToIgnoreTouchesFor = [@[] mutableCopy];
    }
    return _viewsToIgnoreTouchesFor;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    BOOL pointInSide = [super pointInside:point withEvent:event];
    for (UIView *view in self.viewsToIgnoreTouchesFor) {

        CGPoint convertedPoint = [view convertPoint:point fromView:self];
        if ([view pointInside:convertedPoint withEvent:event]) {
            pointInSide = NO;
            break;
        }
    }

    return pointInSide;
}

In your fullscreen viewController where you have the view behind the navBar add these lines to viewDidLoad

UINavigationBarSubclass *navBar = 
(UINavigationBarSubclass*)self.navigationController.navigationBar;
[navBar.viewsToIgnoreTouchesFor addObject:self.buttonBehindBar];

Please note: This will not send touches to the navigationBar, meaning if you add a view which is behind buttons on the navBar the buttons on the navBar will not receive touches.

Swift:

var viewsToIgnore = [UIView]()

override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {

    let ignore = viewsToIgnore.first {
        let converted = $0.convert(point, from: self)
        return $0.point(inside: converted, with: event)
    }
    return ignore == nil && super.point(inside: point, with: event)
}

See the documentation for more info on pointInside:withEvent:

Also if pointInside:withEvent: does not work how you want, it might be worth trying the code above in hitTest:withEvent: instead.


I modified Tricky's solution to work with SwiftUI as an Extension. Works great to solve this problem. Once you add this code to your codebase all views will be able to capture clicks at the top of the screen.

Also posted this alteration to my blog.

import UIKit

/// Passes through all touch events to views behind it, except when the
/// touch occurs in a contained UIControl or view with a gesture
/// recognizer attached
extension UINavigationBar {
    open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        guard nestedInteractiveViews(in: self, contain: point) else { return false }
        return super.point(inside: point, with: event)
    }

    private func nestedInteractiveViews(in view: UIView, contain point: CGPoint) -> Bool {
        if view.isPotentiallyInteractive, view.bounds.contains(convert(point, to: view)) {
            return true
        }

        for subview in view.subviews {
            if nestedInteractiveViews(in: subview, contain: point) {
                return true
            }
        }

        return false
    }
}

private extension UIView {
    var isPotentiallyInteractive: Bool {
        guard isUserInteractionEnabled else { return false }
        return (isControl || doesContainGestureRecognizer)
    }

    var isControl: Bool {
        return self is UIControl
    }

    var doesContainGestureRecognizer: Bool {
        return !(gestureRecognizers?.isEmpty ?? true)
    }
}

Here's a version which doesn't require setting the specific views you'd like to enable underneath. Instead, it lets any touch pass through except if that touch occurs within a UIControl or a view with a UIGestureRecognizer.

import UIKit

/// Passes through all touch events to views behind it, except when the
/// touch occurs in a contained UIControl or view with a gesture
/// recognizer attached
final class PassThroughNavigationBar: UINavigationBar {

    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        guard nestedInteractiveViews(in: self, contain: point) else { return false }
        return super.point(inside: point, with: event)
    }

    private func nestedInteractiveViews(in view: UIView, contain point: CGPoint) -> Bool {

        if view.isPotentiallyInteractive, view.bounds.contains(convert(point, to: view)) {
            return true
        }

        for subview in view.subviews {
            if nestedInteractiveViews(in: subview, contain: point) {
                return true
            }
        }

        return false
    }
}

fileprivate extension UIView {
    var isPotentiallyInteractive: Bool {
        guard isUserInteractionEnabled else { return false }
        return (isControl || doesContainGestureRecognizer)
    }

    var isControl: Bool {
        return self is UIControl
    }

    var doesContainGestureRecognizer: Bool {
        return !(gestureRecognizers?.isEmpty ?? true)
    }
}