👆

macOSでSwiftUIのListの要素をダブルクリックしたイベントをハンドリングしたい

に公開

最初に結論

macOSでSwiftUIのListの要素をダブルクリックしたイベントをハンドリングするには contextMenu(forSelectionType:menu:primaryAction:) modifierを使いましょう。

macOS用SwiftUIアプリでリストの要素をダブルクリック

SwiftUIでリスト表示するmacOSアプリを作っていて、リスト要素をダブルクリックしたときに別ウィンドウで開こうと考えました。

"SwiftUI macOS List ダブルクリック" などで検索すると onTapGesture(count:) modifierで count: 2 を指定することでダブルクリックイベントをハンドリングできるよ、というような記事が見つかります[1]。ですがこの方法は少なくともmacOS 15.5では想定した挙動になりません。

うまくいかない方法: onTapGesture(count:)

onTapGesture(count: 2) を使ってリストの各要素のダブルクリックを受け取れるようにしてみてどういう挙動をするか見てみました。

struct WrongView: View {
    @State private var selection: String? = nil
    let heroes = ["Rean", "Alisa", "Elliot", "Laura", "Machias", "Fie"]

    var body: some View {
        List(heroes, id: \.self, selection: $selection) { hero in
            Text(hero)
                .onTapGesture(count: 2) {
                    debugPrint("Hero \(hero) is double clicked")
                }
        }
        .padding()
    }
}

Textの表示されている幅部分のダブルクリックでは onTapGesture(count:) が発火しますが、それよりも外側では発火しません。

スクリーンショット ダブルクリックが反応しない部分
サンプルアプリのスクリーンショット サンプルアプリのスクリーンショットでダブルクリックが反応しない部分を示す

他にもText部分をシングルクリックしてその行を選択しようとしてもたまに選択されないことがあります。onTapGesture(count:) の方にクリックイベントを奪われているのでしょうか。

ちゃんと動く方法: contextMenu(forSelectionType:menu:primaryAction:)

正解は冒頭にも書いたように contextMenu(forSelectionType:menu:primaryAction:) をListに対して使うことです。引数の3つ目の primaryAction にダブルクリックした要素がSetで渡されます。

struct GoodView: View {
    @State private var selection: String? = nil
    let heroes = ["Rean", "Alisa", "Elliot", "Laura", "Machias", "Fie"]

    var body: some View {
        List(heroes, id: \.self, selection: $selection) { hero in
            Text(hero)
        }
        .padding()
        .contextMenu(forSelectionType: String.self, menu: { _ in }) { heroes in
            debugPrint("Hero \(heroes.first ?? "??") is double clicked")
        }
    }
}

先程のようにリスト要素に onTapGesture(count:) を設定したのと違い、Text部分だけでなく行のどこをダブルクリックしてもちゃんとイベントが発火してくれますしText部分をシングルクリックしたときに選択されないという不自然な挙動も起きません。

まとめ

iOSに比べてmacOSのSwiftUIは利用者も少なくcontextMenu(forSelectionType:menu:primaryAction:) についての情報が少なかったため、今回は啓蒙も兼ねてリストのダブルクリックイベントの検知方法について書きました。

modifierの名前もまぎらわしいんですよね。普通は右クリックメニューのために使うmodifierなのでcontextMenuという名前になっています。そのためどれを使えばいいのだろうとListのmodifier一覧を見ていてもmacOS用のダブルクリックイベント用のmodifierがcontextMenuであることを見つけるのは難しいだろうとも感じました。

AppleのAPIドキュメントページにはprimaryActionの説明にちゃんと「リスト要素のダブルクリック時にprimaryActionが呼び出される」と書いてあります。

Optionally, you can add a custom primary action to the context menu. In macOS, a single click on a row in a selectable container selects that row, and a double click performs the primary action. In iOS and iPadOS, tapping on the row activates the primary action. To select a row without performing an action, either enter edit mode or hold shift or command on a keyboard while tapping the row.
https://developer.apple.com/documentation/swiftui/view/contextmenu(forselectiontype:menu:primaryaction:)#Add-a-primary-action

Swift Assistがきたらこういう検索しづらいUI関係の問題も解決してくれるようになるのかなあ… と思うのでした (まだかなあ)。

関連

脚注
  1. 例えば https://stackoverflow.com/questions/76938707/in-swiftui-on-macos-detect-clicks-and-doubleclicks-in-listonTapGesture(count:) だと想定通りにiOSでは動くがmacOSでは動かないという相談がありましたが、ワークアラウンドとして紹介されている回答も私の環境 (macOS 15) ではリストの自然な挙動になっていませんでした。 ↩︎

Discussion