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:
Sample code for reproducing the bug: https://gist.github.com/rdev/ea0c53448e12835b29faa11fec8e0388
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.
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) }
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With