iOS development
UIView
user interaction
overlapping views
gesture handling

Allowing interaction with a UIView under another UIView

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

By default, UIKit sends touch events to the topmost visible view that accepts interaction. When one view overlays another, underlying views usually stop receiving touches unless you explicitly change hit-testing behavior. The right solution depends on whether the top view should ignore all touches, pass only some regions through, or coordinate gestures with the view below.

How Hit Testing Chooses a Receiver

UIKit starts from the window and asks each view hierarchy branch which view should handle the touch. The system evaluates visibility, alpha, interaction flags, bounds checks, and custom hit-test overrides.

Default behavior means the upper view wins if it returns itself from hit testing. To let a lower view react, you can:

  • disable interaction on the overlay entirely
  • make only selected overlay regions touch-transparent
  • coordinate gesture recognizers so both views can respond

Understanding this flow prevents random fixes that break scrolling or taps elsewhere.

Option 1: Disable Interaction on the Overlay

If the top view is purely visual, the cleanest approach is to disable user interaction on that view.

swift
overlayView.isUserInteractionEnabled = false

Now touches fall through to underlying views naturally. This works well for gradient overlays, decorative masks, and non-interactive hints.

Use this only when the overlay has no controls. If the overlay includes buttons, you need selective pass-through instead.

Option 2: Selective Pass Through With Custom hitTest

When the overlay has interactive and non-interactive areas, subclass and forward touches in transparent zones.

swift
1import UIKit
2
3final class PassthroughView: UIView {
4    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
5        for subview in subviews where !subview.isHidden && subview.alpha > 0 && subview.isUserInteractionEnabled {
6            let converted = subview.convert(point, from: self)
7            if subview.point(inside: converted, with: event) {
8                return true
9            }
10        }
11        return false
12    }
13}

In this example, the overlay only captures touches that hit active subviews. All other touches pass to views underneath.

Usage:

swift
let overlay = PassthroughView(frame: container.bounds)
overlay.backgroundColor = UIColor.black.withAlphaComponent(0.2)
container.addSubview(overlay)

This pattern is common for floating panels where only visible controls should consume touches.

Option 3: Gesture Recognizer Coordination

Sometimes both top and bottom views must react. In that case, gesture recognizer delegates can allow simultaneous recognition.

swift
1import UIKit
2
3final class OverlayController: UIViewController, UIGestureRecognizerDelegate {
4    @IBOutlet weak var overlayView: UIView!
5
6    override func viewDidLoad() {
7        super.viewDidLoad()
8
9        let tap = UITapGestureRecognizer(target: self, action: #selector(handleOverlayTap))
10        tap.delegate = self
11        overlayView.addGestureRecognizer(tap)
12    }
13
14    @objc private func handleOverlayTap() {
15        print("overlay tapped")
16    }
17
18    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
19                           shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
20        return true
21    }
22}

This does not automatically pass all events through, but it helps when pan or tap interactions should coexist.

Debugging Touch Routing

When touches behave unexpectedly, inspect these first:

  • isUserInteractionEnabled
  • alpha and isHidden
  • frame and constraints at runtime
  • recognizer cancellation settings

Quick debug helper:

swift
1override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
2    super.touchesBegan(touches, with: event)
3    print("touch began in \(type(of: self))")
4}

Attach this to suspected views to trace who receives events. Remove afterward to avoid noisy logs.

Practical Layout Example

Suppose you have a map view with a top card panel. You want map panning outside the card controls. A pass-through overlay is ideal:

  • card buttons remain tappable
  • unused overlay space forwards pan and tap to the map
  • no need for brittle gesture hacks

This keeps interaction predictable and aligns with user expectations.

Common Pitfalls

  • Disabling interaction on an overlay that still contains active controls.
  • Overriding hit testing without considering hidden or alpha-zero subviews.
  • Forgetting that some recognizers cancel touches in underlying views.
  • Assuming visual transparency automatically means touch transparency.
  • Using simultaneous gestures as a substitute for proper hit-test design.

Summary

  • UIKit sends touches to the highest eligible view in the hierarchy.
  • For non-interactive overlays, set isUserInteractionEnabled to false.
  • For mixed overlays, implement selective pass-through via hit testing.
  • Use gesture delegate coordination only when both layers need interaction.
  • Debug touch routing with runtime checks before changing architecture.

Course illustration
Course illustration

All Rights Reserved.