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

如何在未触碰HTML元素时让WKWebView穿透触摸事件?

需求与问题

需要在其他视图上方放置WKWebView,要求两者均可交互:WebView内的按钮等元素能正常点击,而WebView透明背景区域的触摸事件要能透传给下层视图。

已尝试方案及痛点

  1. 像素点透明判断方案
    通过self.layer.render获取触摸点像素颜色,判断是否透明,并重写point(inside:with:)方法:
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
    return !isTransparent(point: point)
}

该方案可行,但性能开销较大,每次触摸都要渲染图层并解析像素,会影响页面流畅度。

  1. JavaScript实时判断方案
    尝试通过webView.evaluateJavaScript判断触摸点是否存在可交互元素,但在point或hitTest重写方法中调用时,异步返回的结果总是滞后;若用信号量强制等待结果,会直接导致主线程冻结,应用卡顿。

更优实现方案

方案1:提前缓存Web端可交互区域

核心思路是在Web页面加载完成后,主动获取所有可交互元素的坐标信息并缓存到Native端,触摸时直接判断点是否在缓存区域内,无需实时计算。

实现步骤

  1. 自定义WKWebView子类,重写hitTest(_:with:)方法处理触摸事件;
  2. 在页面加载完成后,通过JavaScript获取所有可交互元素(如按钮、链接、带点击事件的元素)的坐标;
  3. 将Web端坐标转换为Native端可识别的CGRect并缓存;
  4. 触摸时判断点是否在缓存的可交互区域内,决定是否拦截事件或透传。

示例代码

class PassthroughWKWebView: WKWebView {
    private var interactiveFrames: [CGRect] = []
    
    override init(frame: CGRect, configuration: WKWebViewConfiguration) {
        super.init(frame: frame, configuration: configuration)
        navigationDelegate = self
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        navigationDelegate = self
    }
    
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        // 转换触摸点为Web页面内的实际坐标(适配滚动偏移与缩放比例)
        let scrollPoint = convert(point, to: scrollView)
        let adjustedPoint = CGPoint(
            x: scrollPoint.x / scrollView.zoomScale + scrollView.contentOffset.x,
            y: scrollPoint.y / scrollView.zoomScale + scrollView.contentOffset.y
        )
        
        // 检查触摸点是否在可交互元素区域内
        for frame in interactiveFrames {
            if frame.contains(adjustedPoint) {
                return super.hitTest(point, with: event)
            }
        }
        // 不在可交互区域,透传事件给下层视图
        return nil
    }
}

extension PassthroughWKWebView: WKNavigationDelegate {
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        // 执行JS获取所有可交互元素的坐标
        let jsScript = """
        Array.from(document.querySelectorAll('button, a, [onclick], input[type="button"], input[type="submit"]')).map(el => {
            const rect = el.getBoundingClientRect();
            return {x: rect.left, y: rect.top, width: rect.width, height: rect.height};
        });
        """
        webView.evaluateJavaScript(jsScript) { [weak self] result, error in
            guard let self = self, let frameDicts = result as? [[String: CGFloat]] else { return }
            self.interactiveFrames = frameDicts.map { dict in
                CGRect(
                    x: dict["x"] ?? 0,
                    y: dict["y"] ?? 0,
                    width: dict["width"] ?? 0,
                    height: dict["height"] ?? 0
                )
            }
        }
        
        // 监听页面滚动/缩放,按需更新可交互区域(若页面元素位置动态变化)
        scrollView.addObserver(self, forKeyPath: "contentOffset", options: .new, context: nil)
        scrollView.addObserver(self, forKeyPath: "zoomScale", options: .new, context: nil)
    }
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == "contentOffset" || keyPath == "zoomScale" {
            // 页面滚动/缩放后,重新获取可交互区域(可根据实际场景优化,比如仅元素位置变化时更新)
            webView(self, didFinish: nil)
        }
    }
    
    deinit {
        scrollView.removeObserver(self, forKeyPath: "contentOffset")
        scrollView.removeObserver(self, forKeyPath: "zoomScale")
    }
}

方案优势

  • 避免了像素点方案的图层渲染开销,性能更优;
  • 提前缓存可交互区域,不会出现异步JS调用的结果过时或主线程阻塞问题;
  • 若Web页面可控,还可让Web端主动推送可交互区域的变化,进一步优化实时性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 08:23:22