如何让SwiftUI视图检测onHover同时允许点击穿透至下层视图?
SwiftUI实现上层视图悬停检测同时允许下层视图点击
我需要实现这样的视图效果:下层元素保持可点击/可触摸状态,上层元素能检测鼠标悬停。直接给上层元素加.allowsHitTesting(false)的话,下层文本确实可以点击了,但上层的onHover功能就失效了。
最初的SwiftUI代码:
ZStack { Text(content) .textSelection(.enabled) Rectangle() .frame(width: .infinity, height: 10) .onHover { isHovered = $0 } }
我尝试用NSView自定义悬停检测视图,通过重写hitTest返回nil实现点击穿透,虽然基本能用,但悬停检测范围和视图没有完全对齐,代码如下:
class HoverNSView: AnyNSView { var onHover: ((Bool) -> Void)? override func updateTrackingAreas() { super.updateTrackingAreas() if let oldArea = trackingArea { removeTrackingArea(oldArea) } let options: NSTrackingArea.Options = [ .mouseEnteredAndExited, .activeAlways ] let newArea = NSTrackingArea(rect: bounds, options: options, owner: self, userInfo: nil) addTrackingArea(newArea) trackingArea = newArea } override func setFrameSize(_ newSize: NSSize) { super.setFrameSize(newSize) updateTrackingAreas() } override func mouseEntered(with event: NSEvent) { super.mouseEntered(with: event) onHover?(true) } override func mouseExited(with event: NSEvent) { super.mouseExited(with: event) onHover?(false) } /// By returning `nil` here, SwiftUI (and AppKit) will send clicks to underlying views /// instead of stopping at this view. override func hitTest(_: NSPoint) -> NSView? { nil } /// We'll store a reference to our custom NSTrackingArea private var trackingArea: NSTrackingArea? }
修正悬停检测范围对齐问题的方案
问题出在NSTrackingArea的创建和更新逻辑上,调整以下两点即可解决:
- 改用
frame创建跟踪区域:bounds是视图自身坐标系的矩形,当视图在父视图中有偏移时会导致跟踪区域位置偏差;frame是父视图坐标系中的位置和大小,能精准匹配视图实际显示范围。 - 添加
.inVisibleRect选项:确保跟踪区域仅在视图可见范围内生效,避免超出视图边界的误触发。
修改后的HoverNSView代码:
class HoverNSView: NSView { var onHover: ((Bool) -> Void)? override func updateTrackingAreas() { super.updateTrackingAreas() trackingArea.map { removeTrackingArea($0) } let options: NSTrackingArea.Options = [ .mouseEnteredAndExited, .activeAlways, .inVisibleRect ] let newArea = NSTrackingArea(rect: frame, options: options, owner: self, userInfo: nil) addTrackingArea(newArea) trackingArea = newArea } override func setFrame(_ frame: NSRect) { super.setFrame(frame) updateTrackingAreas() } override func mouseEntered(with event: NSEvent) { onHover?(true) } override func mouseExited(with event: NSEvent) { onHover?(false) } override func hitTest(_ point: NSPoint) -> NSView? { nil // 允许点击穿透到下层视图 } private var trackingArea: NSTrackingArea? }
SwiftUI中使用自定义视图
把自定义NSView包装成SwiftUI可调用的视图:
struct HoverOverlay: NSViewRepresentable { var onHover: (Bool) -> Void func makeNSView(context: Context) -> HoverNSView { let view = HoverNSView() view.onHover = onHover view.wantsLayer = true return view } func updateNSView(_ nsView: HoverNSView, context: Context) {} }
在ZStack中替换原有的Rectangle:
ZStack { Text(content) .textSelection(.enabled) HoverOverlay { isHovered in self.isHovered = isHovered } .frame(width: .infinity, height: 10) }
额外优化建议
- 如果视图是圆角或非矩形形状,可在
hitTest中判断点击点是否在视图路径内,进一步精准控制悬停触发范围。 - 若视图frame会动态变化,可在
layoutSubviews方法中也调用updateTrackingAreas,保证跟踪区域实时同步视图大小位置。
内容的提问来源于stack exchange,提问作者Guig
相关产品推荐
相关产品推荐

