You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI如何检测视图外触摸/手势且不拦截底层响应?

问题描述

我用ZStack在屏幕内容上方展示信息框,需求是点击信息框外部时,既要关闭信息框,又让底层屏幕内容能正常响应点击——类似Android里的onTouchOutside功能。

最初我在信息框和底层内容之间加了全屏视图并绑定.onTapGesture,虽然能关闭信息框,但会拦截点击,导致底层内容无法响应。之前看到旧文章说这个功能没法实现,想知道现在SwiftUI是不是还缺这个基础功能?我自定义了TouchPassthroughView也解决不了,点击还是被拦截,有没有可行方案?

现有尝试代码
struct TouchPassthroughView: UIViewRepresentable {
    var tappedOutside: () -> Void

    class Coordinator: NSObject {
        let tappedOutside: () -> Void

        init(tappedOutside: @escaping () -> Void) {
            self.tappedOutside = tappedOutside
        }

        @objc func handleTap(_ sender: UITapGestureRecognizer) {
            tappedOutside()
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(tappedOutside: tappedOutside)
    }

    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        view.backgroundColor = .clear

        let tapGesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTap(_:)))
        tapGesture.cancelsTouchesInView = false
        view.addGestureRecognizer(tapGesture)

        return view
    }

    func updateUIView(_ uiView: UIView, context: Context) {}
}

struct InfoView: View {
    @State private var showBox = true
    
    var body: some View {
        ZStack {
            VStack {
                Text("Screen Content")
                    .padding()
                    .background(Color.blue)
                    .onTapGesture {
                        print("Background tapped")
                    }
            }

            if showBox {
                TouchPassthroughView {
                    print("Tap outside")
                    showBox = false
                }
                .ignoresSafeArea()

                VStack {
                    Text("Box")
                        .padding()
                        .background(Color.white)
                        .cornerRadius(12)
                }
                .frame(width: 200, height: 150)
                .padding(.top, 100)
                .shadow(radius: 10)
            }
        }
    }
} 
解决方案

问题出在你自定义的TouchPassthroughView没有正确实现触摸穿透逻辑——它虽然添加了手势,但视图本身仍然会拦截触摸事件,导致底层内容无法响应。只需要给UIView子类重写hitTest方法,让视图本身不接收触摸,就能实现“既响应外部点击关闭弹窗,又让底层内容正常接收点击”的效果。

修正后的TouchPassthroughView

struct TouchPassthroughView: UIViewRepresentable {
    var tappedOutside: () -> Void

    class Coordinator: NSObject {
        let tappedOutside: () -> Void

        init(tappedOutside: @escaping () -> Void) {
            self.tappedOutside = tappedOutside
        }

        @objc func handleTap(_ sender: UITapGestureRecognizer) {
            tappedOutside()
        }
    }

    // 自定义UIView子类,重写hitTest实现触摸穿透
    class PassthroughView: UIView {
        override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
            let result = super.hitTest(point, with: event)
            // 如果触摸的是视图本身,返回nil让触摸透传到下层;否则返回实际触摸的子视图
            return result == self ? nil : result
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(tappedOutside: tappedOutside)
    }

    func makeUIView(context: Context) -> PassthroughView {
        let view = PassthroughView()
        view.backgroundColor = .clear

        let tapGesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTap(_:)))
        tapGesture.cancelsTouchesInView = false // 不取消下层视图的触摸事件
        view.addGestureRecognizer(tapGesture)

        return view
    }

    func updateUIView(_ uiView: PassthroughView, context: Context) {}
}

使用方式不变

你的InfoView结构无需修改,使用修正后的TouchPassthroughView即可:

struct InfoView: View {
    @State private var showBox = true
    
    var body: some View {
        ZStack {
            VStack {
                Text("Screen Content")
                    .padding()
                    .background(Color.blue)
                    .onTapGesture {
                        print("Background tapped")
                    }
            }

            if showBox {
                TouchPassthroughView {
                    print("Tap outside")
                    showBox = false
                }
                .ignoresSafeArea()

                VStack {
                    Text("Box")
                        .padding()
                        .background(Color.white)
                        .cornerRadius(12)
                }
                .frame(width: 200, height: 150)
                .padding(.top, 100)
                .shadow(radius: 10)
            }
        }
    }
}

原理说明

  1. hitTest重写:PassthroughView的hitTest方法会判断触摸点是否落在视图本身上,如果是则返回nil,让系统将触摸事件传递给下层视图;如果触摸的是视图的子视图(比如信息框),则正常返回子视图,保证信息框自身的触摸不受影响。
  2. 手势配置:cancelsTouchesInView = false确保手势触发时不会取消下层视图的触摸事件,所以底层的.onTapGesture会和弹窗关闭逻辑同时生效。

目前SwiftUI本身确实没有原生的onTouchOutside修饰符,但通过上述自定义UIView的方式可以完美实现需求,不属于无法解决的问题。

内容的提问来源于stack exchange,提问作者Andrei Herford

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 15:42:35