Actually, the code pasted in the answer above is now outdated - check the gist instead. The original code had some issues when list was scrolled. Updated gist uses an NSView to detect the clicks instead
extension View {
/// Adds a double click handler this view (macOS only)
///
/// Example
/// ```
/// Text("Hello")
/// .onDoubleClick { print("Double click detected") }
/// ```
/// - Parameters:
/// - handler: Block invoked when a double click is detected
func onDoubleClick(handler: @escaping () -> Void) -> some View {
modifier(DoubleClickHandler(handler: handler))
}
}
struct DoubleClickHandler: ViewModifier {
let handler: () -> Void
func body(content: Content) -> some View {
content.overlay {
DoubleClickListeningViewRepresentable(handler: handler)
}
}
}
struct DoubleClickListeningViewRepresentable: NSViewRepresentable {
let handler: () -> Void
func makeNSView(context: Context) -> DoubleClickListeningView {
DoubleClickListeningView(handler: handler)
}
func updateNSView(_ nsView: DoubleClickListeningView, context: Context) {}
}
class DoubleClickListeningView: NSView {
let handler: () -> Void
init(handler: @escaping () -> Void) {
self.handler = handler
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
if event.clickCount == 2 {
handler()
}
}
}
Post
Replies
Boosts
Views
Activity
I made a modifier which seems to do the trick. Uses an NSView to do the actual handling: https://gist.github.com/joelekstrom/91dad79ebdba409556dce663d28e8297
extension View {
/// Adds a double click handler this view (macOS only)
///
/// Example
/// ```
/// Text("Hello")
/// .onDoubleClick { print("Double click detected") }
/// ```
/// - Parameters:
/// - handler: Block invoked when a double click is detected
func onDoubleClick(handler: @escaping () -> Void) -> some View {
modifier(DoubleClickHandler(handler: handler))
}
}
struct DoubleClickHandler: ViewModifier {
let handler: () -> Void
func body(content: Content) -> some View {
content.overlay {
DoubleClickListeningViewRepresentable(handler: handler)
}
}
}
struct DoubleClickListeningViewRepresentable: NSViewRepresentable {
let handler: () -> Void
func makeNSView(context: Context) -> DoubleClickListeningView {
DoubleClickListeningView(handler: handler)
}
func updateNSView(_ nsView: DoubleClickListeningView, context: Context) {}
}
class DoubleClickListeningView: NSView {
let handler: () -> Void
init(handler: @escaping () -> Void) {
self.handler = handler
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
if event.clickCount == 2 {
handler()
}
}
}
I had the same question and looked around but couldn't find a gesture based solution which works in all cases, especially not with List that has selection states. I made a view modifier using an NSEvent click monitor which seems to solve it as expected for all cases I've tried:
extension View {
/// Adds a double click handler this view (macOS only)
///
/// Example
/// ```
/// Text("Hello")
/// .onDoubleClick { print("Double click detected") }
/// ```
/// - Parameters:
/// - inset: To shrink or expand the double click hit area of the view, pass positive or negative values, respectively
/// - handler: Block invoked when a double click is detected
func onDoubleClick(inset: CGSize = .zero, handler: @escaping () -> Void) -> some View {
modifier(DoubleClickHandler(inset: inset, handler: handler))
}
}
struct DoubleClickHandler: ViewModifier {
let inset: CGSize
let handler: () -> Void
@State private var monitor: Any?
private func addListener(frame: NSRect) {
let adjustedFrame = frame.insetBy(dx: inset.width, dy: inset.height)
monitor = NSEvent.addLocalMonitorForEvents(matching: .leftMouseDown) {
if $0.clickCount == 2 {
var location = $0.locationInWindow
location.y = $0.window!.frame.size.height - location.y // Flip to fix coordinate system mismatch
if adjustedFrame.contains(location) { handler() }
}
return $0
}
}
private func removeListener() {
if let monitor = monitor {
NSEvent.removeMonitor(monitor)
}
}
func body(content: Content) -> some View {
content.background {
GeometryReader { proxy in
Color.clear
.onAppear { addListener(frame: proxy.frame(in: .global)) }
.onDisappear { removeListener() }
}
}
}
}
https://gist.github.com/joelekstrom/91dad79ebdba409556dce663d28e8297