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

如何让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的创建和更新逻辑上,调整以下两点即可解决:

  1. 改用frame创建跟踪区域:bounds是视图自身坐标系的矩形,当视图在父视图中有偏移时会导致跟踪区域位置偏差;frame是父视图坐标系中的位置和大小,能精准匹配视图实际显示范围。
  2. 添加.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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 22:33:11