NavigationLink double click on List for MacOS

I need the whole row (including space without content) within the NavigationLink to accept a double click gesture as well as the standard single click that will activate the row and display in the Detail View.

If I use an
Code Block
onTapGesture(count:2)
or
Code Block
simultaneousGesture(TapGesture(count:2)
it doesn't quite work as expected. The content of the row will trigger the double click action but not the normal single click behaviour of the NavigationLink and requires clicking in an unpopulated area of the row. I have tried another onTapGesture with count:1 after the first to explicitly tell it to display the detail view which works but does not highlight the now selected row.

I'd like to be able to double or single click anywhere in the list row, with a single click highlighting the row and displaying the detail view and a double click should do the same as the single click plus trigger an action. Similar to how Finder's column view works.

If anyone could help with this it would be greatly appreciated.

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

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

I had the same problem in a NavigationSplitView and managed to solve it using simultaneous gestures as you originally tried. To make sure single-clicking fully matches native behaviour, my single-click handler sets the selection which is passed as a binding to the List, and it also sets a FocusState.

struct ContentView: View {
    // Assume there's an "items" array of Item instances to display.

    @State private var selectedItem: Item?
    @FocusState var listFocused: Bool
    
    var body: some View {
        NavigationSplitView {
            List(selection: $selectedItem) {
                ForEach(items) { item in
                    NavigationLink(value: item) { Text(verbatim: item.name) }
                        .gesture(doubleClickGesture(for: item))
                }
            }
            .focused($listFocused)
        } detail: {
            DetailView(item)
        }
    }
    
    private func doubleClickGesture(for item: Item) -> some Gesture {
        SimultaneousGesture(
            TapGesture(count: 1).onEnded {
                selectedItem = item
                listFocused = true
            },
            TapGesture(count: 2).onEnded {
                print(“Double-clicked”)
            }
        )
    }
}
NavigationLink double click on List for MacOS
 
 
Q