BS
BleepingSwift
Published on

> Disable Touches with allowsHitTesting in SwiftUI

Authors
  • avatar
    Name
    Mick MacCallum
    Twitter
    @0x7fs

Sometimes you need a view to be visible but not interactive. Maybe you have an overlay that shouldn't block touches, a decorative element on top of buttons, or a loading state that should let users interact with content behind it. The allowsHitTesting(_:) modifier controls whether a view participates in hit testing, letting you make views "transparent" to touches.

Basic Usage

When you set allowsHitTesting(false) on a view, taps pass right through it as if it wasn't there:

struct OverlayExample: View {
    var body: some View {
        ZStack {
            Button("Tap Me") {
                print("Button tapped!")
            }
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(8)

            Rectangle()
                .fill(Color.red.opacity(0.3))
                .allowsHitTesting(false)
        }
    }
}

The red overlay covers the button visually, but taps go right through to the button underneath. Without .allowsHitTesting(false), the rectangle would intercept all touches.

Difference from disabled

You might wonder how this differs from the .disabled() modifier. The key difference is what happens to touches:

VStack(spacing: 20) {
    // disabled: button looks dimmed, absorbs taps (nothing happens)
    Button("Disabled Button") { print("Won't print") }
        .disabled(true)

    // allowsHitTesting(false): button looks normal, taps pass through
    Button("No Hit Testing") { print("Won't print") }
        .allowsHitTesting(false)
}

With .disabled(true), the view still receives the touch but ignores it. With .allowsHitTesting(false), the view doesn't receive the touch at all—it continues to whatever is behind. Also, .disabled() changes the view's appearance to indicate it's inactive, while .allowsHitTesting() has no visual effect.

Practical Example: Decorative Overlays

A common use case is adding visual effects that shouldn't interfere with interaction:

struct GlowingButton: View {
    @State private var isGlowing = false

    var body: some View {
        ZStack {
            Button("Start") {
                print("Started!")
            }
            .padding(.horizontal, 40)
            .padding(.vertical, 15)
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(10)

            if isGlowing {
                RoundedRectangle(cornerRadius: 10)
                    .stroke(Color.blue, lineWidth: 2)
                    .frame(width: 130, height: 50)
                    .scaleEffect(1.2)
                    .opacity(0.5)
                    .allowsHitTesting(false)
            }
        }
        .onAppear {
            withAnimation(.easeInOut(duration: 1).repeatForever()) {
                isGlowing = true
            }
        }
    }
}

The pulsing glow effect sits on top of the button but doesn't prevent taps from reaching it.

Conditional Hit Testing

You can make hit testing conditional based on state:

struct ConditionalInteraction: View {
    @State private var isLocked = true

    var body: some View {
        VStack(spacing: 20) {
            Toggle("Unlock Controls", isOn: Binding(
                get: { !isLocked },
                set: { isLocked = !$0 }
            ))
            .padding()

            VStack(spacing: 15) {
                Button("Action 1") { print("Action 1") }
                Button("Action 2") { print("Action 2") }
                Button("Action 3") { print("Action 3") }
            }
            .padding()
            .background(Color.gray.opacity(0.1))
            .cornerRadius(12)
            .allowsHitTesting(!isLocked)
            .opacity(isLocked ? 0.5 : 1)
        }
    }
}

When locked, the entire button group ignores touches. The opacity change provides visual feedback that the controls are inactive.

Loading Overlay Pattern

A semi-transparent loading overlay that still allows some interaction:

struct LoadingOverlay: View {
    @State private var isLoading = false
    @State private var count = 0

    var body: some View {
        ZStack {
            VStack(spacing: 20) {
                Text("Count: \(count)")
                    .font(.title)

                Button("Increment") {
                    count += 1
                }
                .buttonStyle(.borderedProminent)

                Button("Simulate Load") {
                    isLoading = true
                    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                        isLoading = false
                    }
                }
            }

            if isLoading {
                Color.black.opacity(0.3)
                    .ignoresSafeArea()
                    .allowsHitTesting(true) // blocks all interaction

                ProgressView()
                    .scaleEffect(1.5)
                    .tint(.white)
            }
        }
    }
}

During loading, the overlay has allowsHitTesting(true) (the default) which blocks all touches. Change it to false if you want users to still interact while loading.

Passthrough Gesture Areas

Create views with specific interactive and non-interactive zones:

struct PartialInteraction: View {
    var body: some View {
        ZStack {
            // Background content that should receive taps
            VStack {
                ForEach(0..<5) { i in
                    Button("Item \(i)") {
                        print("Tapped item \(i)")
                    }
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color.blue.opacity(0.1))
                }
            }

            // Floating panel that blocks only its area
            VStack {
                Spacer()

                HStack {
                    Spacer()

                    VStack {
                        Text("Quick Actions")
                            .font(.headline)
                        Button("Share") { }
                        Button("Save") { }
                    }
                    .padding()
                    .background(.regularMaterial)
                    .cornerRadius(12)
                    .padding()
                }
            }
        }
    }
}

The floating panel naturally blocks touches in its area while the rest of the screen remains interactive.

Hit Testing with Animations

Be careful with animated views—you might want hit testing to follow the animation:

struct AnimatedOverlay: View {
    @State private var showOverlay = false

    var body: some View {
        ZStack {
            Button("Main Action") {
                print("Main action!")
            }
            .buttonStyle(.borderedProminent)

            Rectangle()
                .fill(Color.black.opacity(showOverlay ? 0.5 : 0))
                .allowsHitTesting(showOverlay)
                .ignoresSafeArea()

            Button("Toggle Overlay") {
                withAnimation {
                    showOverlay.toggle()
                }
            }
            .offset(y: 100)
        }
    }
}

The allowsHitTesting value changes immediately with the state, even though the opacity animates. This is usually what you want—interaction should toggle instantly, not gradually.

Important Notes

The allowsHitTesting modifier affects the view and all its children. If you need different hit testing behavior for child views, apply the modifier at the appropriate level:

VStack {
    Button("This is tappable") { }

    Button("This is not") { }
        .allowsHitTesting(false)

    Button("This is also tappable") { }
}

Also remember that allowsHitTesting(false) only affects touches—the view still renders normally and takes up space in the layout. It's purely about whether the view responds to user interaction.

Use allowsHitTesting whenever you need visual overlays that don't block interaction, or when you want to selectively enable and disable touch handling without changing a view's appearance.

subscribe.sh

// Stay Updated

Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.

>

By subscribing, you agree to our Privacy Policy.