如果鼠标移动太快,SwiftUI onHover 不会注册鼠标离开元素

Posted

技术标签:

【中文标题】如果鼠标移动太快,SwiftUI onHover 不会注册鼠标离开元素【英文标题】:SwiftUI onHover doesn't register mouse leaving the element if mouse moves too fast 【发布时间】:2021-01-22 07:58:45 【问题描述】:

我在 SwiftUI 中制作了一些自定义滑块视图,它们根据悬停状态更改外观,但如果鼠标移出太快(这实际上是移动光标的非常合理的速度),它会一直保持悬停状态,直到您重新悬停并缓慢地重新离开组件。

有解决办法吗? 悬停代码非常标准:

struct RulerSlider: View 
  @State var hovering = false

  var body: some View 
    GeometryReader  geometry in
      ZStack 
        // Ruler lines
        if hovering 
          Ruler()
        
      
      .onHover  hover in
        withAnimation(.easeOut(duration: 0.1)) 
          self.hovering = hover
        
      
    
  

问题如下:

重现错误的示例代码: https://gist.github.com/rdev/ea0c53448e12835b29faa11fec8e0388

【问题讨论】:

尝试无动画,尝试使行(在父视图中)唯一。如果没有帮助,请准备独立的最小可重现示例进行调试。 我尝试在没有动画的情况下进行,结果相同。如果鼠标移出视图的速度不够慢,则似乎不会触发“mouseleave”事件(或与之等效的任何事件) 添加了一个带有可重现代码的要点链接。虽然在精简示例中不会发生太多,但仍然会发生。 我在我的应用程序中也看到了这一点。即使背景试图在鼠标退出时为零。 【参考方案1】:

我今天用一个空的 NSView 上的跟踪区域解决了这个问题。这是在一个半复杂且快速刷新的网格视图中测试的,该视图之前具有您所描绘的相同行为。大约 75 个视图在 GIF capture in this gist 中应用了此修饰符,大多数彼此之间的边框为零。

呼叫站点的糖

import SwiftUI

extension View 
    func whenHovered(_ mouseIsInside: @escaping (Bool) -> Void) -> some View 
        modifier(MouseInsideModifier(mouseIsInside))
    

用空的跟踪视图表示

struct MouseInsideModifier: ViewModifier 
    let mouseIsInside: (Bool) -> Void
    
    init(_ mouseIsInside: @escaping (Bool) -> Void) 
        self.mouseIsInside = mouseIsInside
    
    
    func body(content: Content) -> some View 
        content.background(
            GeometryReader  proxy in
                Representable(mouseIsInside: mouseIsInside,
                              frame: proxy.frame(in: .global))
            
        )
    
    
    private struct Representable: NSViewRepresentable 
        let mouseIsInside: (Bool) -> Void
        let frame: NSRect
        
        func makeCoordinator() -> Coordinator 
            let coordinator = Coordinator()
            coordinator.mouseIsInside = mouseIsInside
            return coordinator
        
        
        class Coordinator: NSResponder 
            var mouseIsInside: ((Bool) -> Void)?
            
            override func mouseEntered(with event: NSEvent) 
                mouseIsInside?(true)
            
            
            override func mouseExited(with event: NSEvent) 
                mouseIsInside?(false)
            
        
        
        func makeNSView(context: Context) -> NSView 
            let view = NSView(frame: frame)
            
            let options: NSTrackingArea.Options = [
                .mouseEnteredAndExited,
                .inVisibleRect,
                .activeInKeyWindow
            ]
            
            let trackingArea = NSTrackingArea(rect: frame,
                                              options: options,
                                              owner: context.coordinator,
                                              userInfo: nil)
            
            view.addTrackingArea(trackingArea)
            
            return view
        
        
        func updateNSView(_ nsView: NSView, context: Context) 
        
        static func dismantleNSView(_ nsView: NSView, coordinator: Coordinator) 
            nsView.trackingAreas.forEach  nsView.removeTrackingArea($0) 
        
    

【讨论】:

@Ryan - 你有没有可能这么聪明! :-) 非常感谢。为什么 onHover() 上的 Apples 修改后只使用这个?? @DuncanGroenewald 哈哈我不知道我是不是......但如果是这样,请雇用我作为初级开发人员! @Ryan - 实际上存在一个问题,因为它阻止了其他手势的发生,例如 .onTapGesture() 必须在 .whenHovered() 修饰符之外。所以你需要把它放在任何需要响应手势事件的控件下面。例如 HStack Toggle($isOn, label:... ..overlay(...).whenHovered .. 表示 Toggle() 不会得到任何鼠标事件。 @DuncanGroenewald 你能用代码打开一个新问题吗?在我自己的项目中将 .whenHovered 应用于 Toggle 外部或将手势应用于 Toggle 标签时,我没有观察到干扰。如果没有 .whenHovered,在 Toggle 之外应用轻击手势将不起作用。 太棒了!完美运行【参考方案2】:

onHover 如果您为每个单独的子视图应用它,则它是滞后的。如果您在父容器视图中应用它,它会按预期工作。这是一个例子:

struct ContainerView: View 
    var elements = (0..<10).map  "\($0)" 
    
    @State private var lastHoveredId = ""
    
    var body: some View 
        ScrollView 
            ForEach(elements, id: \.self)  element in
                ChildView(lastHoveredId: $lastHoveredId, id: element)
                    .onHover  isHovered in
                        if isHovered 
                            lastHoveredId = element
                         else if lastHoveredId == element 
                            lastHoveredId = ""
                        
                    
            
        
    


struct ChildView: View 
    @Binding var lastHoveredId: String
    var id: String
    
    @State private var isHovered = false
    
    var body: some View 
        Text(id)
            .frame(width: 100, height: 30)
            .background(isHovered ? Color.primary.opacity(0.2) : .clear)
            .animation(.easeIn(duration: 0.2))
            .onChange(of: lastHoveredId) 
                isHovered = $0 == id
            
    

【讨论】:

【参考方案3】:

您可以通过在容器视图中将onHover 实际应用到您的RulerSliders 来轻松修复此问题,而无需离开 SwiftUI 环境(与公认的答案不同)。换句话说,跟踪当前悬停在List 中的元素(如果有),而不是列出子元素。

public struct RulersList 
    @State private var hoveredRulerId: RulerModel.ID? = nil
    public let rulers: [RulerModel]
    public var body: some View 
        List 
            ForEach(rulers)  ruler in
                RulerSlider(ruler, ruler.id == hoveredRulerId)
                    .onHover(perform:  isHovering in
                        self.hoveredRulerId = isHovering ? ruler.id : nil
                    )
            
        
    


public struct RulerSlider: View 
    public let ruler: RulerModel
    public var isHovered
    public var body: some View 
        Stuff()
            .background(isHovered ? Color.accentColor.opacity(0.10) : Color.clear)
            // DON'T DO onHover HERE, IT HAS SEVERE PERFORMANCE PENALTY
    

请注意ForEach 中的RulerSlider(ruler, ruler.id == hoveredRulerId),这是您通知子视图是否被悬停的方式。

【讨论】:

非常有趣!整洁的。你知道为什么会这样吗?很高兴知道有这个选项,虽然这太糟糕了,原生方式需要父母知道和管理孩子的详细信息。 我猜是性能?如果您为列表中的每个子视图应用onHover 修饰符,则必须为每个子视图分别计算跟踪光标焦点和更新绑定值,每个子视图都需要单独的原子事务。但是,如果您将容器本身设置为跟踪元素,那么您只需检查光标的 CGPoint 是否在任何子视图的框架内,然后更新同一事务中的所有依赖绑定。但我无权访问 SwiftUI 源,所以我不确定。 有道理。使用数百个 AppKit NSResponder 比 child-modifier 响应更快,而且成本也不高。实现有些奇怪。

以上是关于如果鼠标移动太快,SwiftUI onHover 不会注册鼠标离开元素的主要内容,如果未能解决你的问题,请参考以下文章

在图例圆环图的 onHover 期间更快地加载 Tooltip

急求解决方法 鼠标移动太快时 onmouseout事件无法触发

鼠标快速移动时不会触发jQuery mouseleave函数

如何在 SwiftUI 中控制 iPadOS 上的鼠标位置

如何获取自定义形状的 .onHover(...) 事件

原始战争之主界面滑动效果