如何在未触碰HTML元素时让WKWebView穿透触摸事件?
需求与问题
需要在其他视图上方放置WKWebView,要求两者均可交互:WebView内的按钮等元素能正常点击,而WebView透明背景区域的触摸事件要能透传给下层视图。
已尝试方案及痛点
- 像素点透明判断方案
通过self.layer.render获取触摸点像素颜色,判断是否透明,并重写point(inside:with:)方法:
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { return !isTransparent(point: point) }
该方案可行,但性能开销较大,每次触摸都要渲染图层并解析像素,会影响页面流畅度。
- JavaScript实时判断方案
尝试通过webView.evaluateJavaScript判断触摸点是否存在可交互元素,但在point或hitTest重写方法中调用时,异步返回的结果总是滞后;若用信号量强制等待结果,会直接导致主线程冻结,应用卡顿。
更优实现方案
方案1:提前缓存Web端可交互区域
核心思路是在Web页面加载完成后,主动获取所有可交互元素的坐标信息并缓存到Native端,触摸时直接判断点是否在缓存区域内,无需实时计算。
实现步骤
- 自定义
WKWebView子类,重写hitTest(_:with:)方法处理触摸事件; - 在页面加载完成后,通过JavaScript获取所有可交互元素(如按钮、链接、带点击事件的元素)的坐标;
- 将Web端坐标转换为Native端可识别的
CGRect并缓存; - 触摸时判断点是否在缓存的可交互区域内,决定是否拦截事件或透传。
示例代码
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
相关产品推荐
相关产品推荐

