Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI onHover doesn't register mouse leaving the element if mouse moves too fast

I've made some custom slider views in SwiftUI that change appearance based on hover state, but if the mouse moves out too fast (which is actually a very reasonable speed of moving a cursor), it stays in the hover state until you re-hover and re-leave the component slowly.

Is there a solution for this? The hover code is pretty standard:

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
        }
      }
    }
  }
}

Here's what the issue looks like:

enter image description here

Sample code for reproducing the bug: https://gist.github.com/rdev/ea0c53448e12835b29faa11fec8e0388

like image 593
max Avatar asked Jan 22 '21 07:01

max


1 Answers

I resolved this issue today with a tracking area on an empty NSView. This is tested in a semi-complex and quickly refreshing grid view, which previously had the same behavior you pictured. About 75 views have this modifier applied in the GIF capture in this gist, most with zero border to each other.

Sugar for call site

import SwiftUI

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

Representable with empty tracking view

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) }
        }
    }
}
like image 105
Ryan Avatar answered Oct 06 '22 17:10

Ryan