Binde für eine Mac-Elementliste die Auswahl an eine SwiftUI-List und stelle über deren auswahlbezogenes Kontextmenü eine primäre Aktion bereit. Ein einfacher Klick wählt die Zeile aus; ein Doppelklick öffnet sie. Halte den Auswahlzustand getrennt vom Präsentationszustand.
Dieses Tutorial implementiert diese Interaktion in deiner eigenen macOS-App mit drei synthetischen Elementen im Arbeitsspeicher. Außerdem werden die Grenzen für eigene SwiftUI-Oberflächen und AppKit-Ansichten erläutert, damit du einen Ansatz wählen kannst, ohne dass mehrere Ebenen denselben Klick verarbeiten.
Die primäre Aktion des auswählbaren Containers verwenden
Apple dokumentiert für auswählbare Container unter macOS ausdrücklich die Auswahl per einfachem Klick und primäre Aktionen per Doppelklick. Der unterstützte SwiftUI-Einstiegspunkt ist contextMenu(forSelectionType:menu:primaryAction:), verfügbar ab macOS 13. Füge ihn einem Container hinzu, der die Auswahl unterstützt, etwa List oder Table. Apples Dokumentation zu auswahlbezogenen Kontextmenüs.
Drei Aufgaben sind voneinander getrennt: Das Modell identifiziert jedes Element, die Liste verwaltet die Auswahl über eine Bindung und dein Handler für die primäre Aktion legt fest, was „Öffnen“ bedeutet. In diesem Beispiel wird beim Öffnen ein Detailblatt für das Element angezeigt. Die Auswahl eines Elements ändert lediglich die hervorgehobene Zeile und die Auswahlbeschriftung.
Wähle die Implementierung passend zu der Ansicht, die für die Interaktion zuständig ist:
| Deine Oberfläche | Geeigneter Ausgangspunkt | Zuständigkeit, die bei dir bleibt |
|---|---|---|
| Eine SwiftUI-Elementliste oder -Tabelle unter macOS 13 oder neuer | Auswahlbindung plus auswahlbezogene primäre Aktion | Aktions-IDs auflösen und die zugehörigen Inhalte anzeigen |
| Eine eigene SwiftUI-Kachel oder -Form | Explizite Gestenkombination | Auswahloptik, Aktivierungsrichtlinie und weitere Eingabepfade |
| Eine in SwiftUI eingebettete AppKit-Tabelle | NSTableView-Auswahl und doubleAction |
Auswahl und Aktivierung an den SwiftUI-Zustand anbinden |
| Eine eigene AppKit-Zeichenfläche | Eigene Mausereignisverarbeitung | Trefferprüfung, Elementidentität und Richtlinie für Klickfolgen |
Der erste Ansatz passt direkt zu dieser Aufgabe. Die eigenen Varianten sind nützlich, wenn die Oberfläche tatsächlich eine andere Form benötigt; dafür muss auch mehr Interaktionsverhalten selbst implementiert werden. Apple dokumentiert die zugrunde liegenden Alternativen unter Gestenkombinationen, Doppelklickaktionen bei AppKit-Tabellen und Behandlung von Maus-Down-Ereignissen.
Ein vollständiges Beispiel mit Einzelauswahl erstellen
Erstelle in Xcode ein macOS-App-Projekt mit einer SwiftUI-Oberfläche und setze das Deployment-Ziel auf macOS 13 oder neuer. Ersetze die generierten App- und Content-View-Deklarationen durch die folgende einzelne Datei. Im Target darf es nur eine @main-App-Deklaration geben.
Das Programm verwendet feste Ganzzahl-IDs, drei farbige Kreise und lokalen Zustand. Es liest keine Dateien und öffnet keine externen Ressourcen. Die Symbolleisten-Schaltfläche Open und der Kontextmenübefehl Open rufen dieselbe Funktion auf wie die Aktivierung per Doppelklick.
import SwiftUI
struct SampleItem: Identifiable {
let id: Int
let name: String
let tint: Color
}
@main
struct ClickSelectionDemoApp: App {
var body: some Scene {
WindowGroup {
ItemBrowser()
}
}
}
struct ItemBrowser: View {
private let items = [
SampleItem(id: 1, name: "Circle A", tint: .blue),
SampleItem(id: 2, name: "Circle B", tint: .orange),
SampleItem(id: 3, name: "Circle C", tint: .purple)
]
@State private var selectedID: SampleItem.ID? = nil
@State private var openedItem: SampleItem? = nil
@State private var message = "Select an item."
private var selectedItem: SampleItem? {
items.first { $0.id == selectedID }
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text(selectedItem.map {
"Selected: \($0.name)"
} ?? "No selection")
Spacer()
Button("Open") {
if let id = selectedID {
openItems([id])
}
}
.disabled(selectedItem == nil)
}
List(items, selection: $selectedID) { item in
HStack(spacing: 10) {
Circle()
.fill(item.tint)
.frame(width: 18, height: 18)
Text(item.name)
}
.padding(.vertical, 4)
}
.contextMenu(forSelectionType: SampleItem.ID.self) { ids in
if ids.count == 1 {
Button("Open") {
openItems(ids)
}
}
} primaryAction: { ids in
openItems(ids)
}
Text(message)
.font(.caption)
}
.padding()
.frame(minWidth: 360, minHeight: 280)
.sheet(item: $openedItem) { item in
VStack(spacing: 16) {
Circle()
.fill(item.tint)
.frame(width: 80, height: 80)
Text(item.name)
.font(.title2)
Button("Close") {
openedItem = nil
}
}
.padding(32)
.frame(minWidth: 260, minHeight: 220)
}
}
private func openItems(_ ids: Set<SampleItem.ID>) {
guard ids.count == 1, let id = ids.first else {
message = "Choose exactly one item to open."
return
}
guard let item = items.first(where: { $0.id == id }) else {
message = "That item is no longer available."
return
}
openedItem = item
message = "Opened: \(item.name)"
}
}
Die Deklarationen und Referenzen wurden geprüft, aber das Beispiel wurde hier weder kompiliert noch ausgeführt: In der verfügbaren Umgebung gibt es weder eine Swift-Toolchain noch eine macOS-GUI. Baue es in Xcode und führe die folgenden Interaktionsprüfungen durch.
Identität, Auswahl und Öffnen getrennt halten
SampleItem entspricht dem Swift-Protokoll Identifiable. Seine feste id steht bei Ansichtsaktualisierungen für dasselbe Element. Apple definiert dieses Protokoll anhand einer stabilen Identität; der Anzeigename ist eine separate Eigenschaft. Bewahre in einer größeren App die ID eines Elements, wenn sich sein Name oder Erscheinungsbild ändert. Apples Dokumentation zu Identifiable.
Das optionale selectedID erzeugt eine Liste mit Einzelauswahl. Eine Menge von IDs würde stattdessen Mehrfachauswahl ermöglichen. Apples Dokumentation zu Listen beschreibt diese beiden Bindungsformen und die vom Container bereitgestellte Auswahloptik. Apples List-Dokumentation.
openedItem steuert die Präsentation unabhängig davon. Wenn es auf ein Element gesetzt wird, erscheint das Blatt; durch Setzen auf nil wird es geschlossen. Das entspricht der dokumentierten API für elementgesteuerte Blätter. Da beim Auswählen einer Zeile nie openedItem gesetzt wird, kann die Auswahl allein in dieser Implementierung das Blatt nicht auslösen. Apples Dokumentation zu elementgesteuerten Blättern.
Die an die Aktion übergebenen IDs auflösen
Die Kontextmenü-API übernimmt den ID-Typ des Containers, und ihre Aktion erhält eine Menge von IDs. Das Beispiel verwendet daher SampleItem.ID.self, passend zur Listenauswahl. Es löst das Argument des Callbacks auf, statt einen separat ausgelesenen Auswahlwert einzusetzen. Apples Dokumentation zu Kontextmenüparametern.
Der Handler akzeptiert absichtlich genau eine ID. Bei einer leeren Menge wird eine Meldung angezeigt, bei einer nicht auflösbaren ID eine andere. Wenn du später Mehrfachauswahl aktivierst, lege zuerst fest, wie das Öffnen funktionieren soll: eine Detailansicht für eine Gruppe anzeigen, separate Fenster öffnen oder eine einzelne Auswahl verlangen. Ein beliebiges erstes Element zu nehmen, würde eine undefinierte Richtlinie verschleiern.
Warum zwei Handler für Tippgesten eine andere Entwurfsentscheidung erfordern
Eine eigene Kachel benötigt möglicherweise eigene Gesten. SwiftUIs onTapGesture(count:perform:) akzeptiert eine erforderliche Anzahl von Klicks oder Tippgesten. Die Dokumentation belegt jedoch nicht, dass zwei unabhängige Modifikatoren auf einer Ansicht einen exklusiven Vertrag für einfachen gegenüber doppeltem Klick schaffen. Apples Dokumentation zu Tippaktionen.
Lege zuerst fest, was bei einem Doppelklick mit der Auswahl geschieht. Bei einer gewöhnlichen Elementübersicht ist es sinnvoll und konsistent mit der containerbasierten Lösung, das Element vor dem Öffnen auszuwählen. Eine separate Anforderung, die Aktion für den einfachen Klick nur auszuführen, wenn die Folge nicht zu einem Doppelklick wird, erfordert eine Abstimmung.
Für eine eigene Oberfläche bietet SwiftUI exclusively(before:): Es ist nur eine Komponentengeste erfolgreich, wobei die erste Vorrang hat. Das Folgende ist ein Gestenausdruck, kein vollständiger Ersatz für das Listenbeispiel: Apples Dokumentation zu exklusiven Gesten.
TapGesture(count: 2)
.exclusively(before: TapGesture(count: 1))
Dadurch wird die Erkennung des Doppelklicks priorisiert. Hänge die kombinierte Geste an das eigene Element und verarbeite ihr Ergebnis entsprechend deiner Auswahl- und Öffnungsrichtlinie. TapGesture dokumentiert die Erkennung anhand der erforderlichen Anzahl von Tippgesten. Apples Dokumentation zu TapGesture.
Diese Richtlinie hat eine zeitliche Folge: Um festzustellen, dass eine Folge ausschließlich aus einem einfachen Klick besteht, muss ein Doppelklick ausgeschlossen werden. Das ergibt sich aus der Interaktionsanforderung und ist keine dokumentierte feste Verzögerung in SwiftUI. Versprich keine sofortige Ausführung des einfachen Klicks, wenn zugleich sichergestellt sein muss, dass sie nie während einer Doppelklickfolge erfolgt.
Lege bei einer eigenen Kachel außerdem den vorgesehenen anklickbaren Bereich fest. contentShape(Rectangle()) kann dafür sorgen, dass ein gepolsterter rechteckiger Bereich an der Trefferprüfung teilnimmt; die Vorrangigkeit von Gesten wird dadurch nicht festgelegt. Halte diese beiden Aspekte getrennt, wenn du verpasste Klicks untersuchst. Apples Dokumentation zu Inhaltsformen.
Bei AppKit die Zuständigkeit für Ereignisse bewahren
Wenn eine vorhandene AppKit-Tabelle für die Zeilen zuständig ist, verwende den unterstützten Aktivierungsmechanismus dieser Tabelle. NSTableView.doubleAction sendet bei einem Doppelklick einen Selektor an sein Ziel. Apple dokumentiert außerdem clickedRow und clickedColumn, um festzustellen, wo die Aktivierung stattgefunden hat, auch bei Spaltenüberschriften. Prüfe vor dem Öffnen, ob die Position ein Element bezeichnet. Apples Dokumentation zu doubleAction.
Bei einer eigenen AppKit-Oberfläche empfängt mouseDown(with:) ein Ereignis für das Drücken der linken Maustaste, und NSEvent.clickCount gibt die mit Maus-Down- oder Maus-Up-Ereignissen verknüpfte Klickanzahl an. Das Lesen dieser Eigenschaft bei einem nicht zugehörigen Ereignis liegt außerhalb ihres dokumentierten Vertrags und kann eine Ausnahme auslösen. Beschränke diese Auswertung auf die Ansicht, die das passende Mausereignis verarbeitet. Apples Dokumentation zu Maus-Down-Ereignissen, Apples Dokumentation zur Klickanzahl.
Verwende NSViewRepresentable, wenn du diese zuständige AppKit-Ansicht in SwiftUI einbettest. Erstelle sie in makeNSView, synchronisiere die aktuelle Konfiguration in updateNSView und verwende einen Coordinator, wenn Ziel-Aktion- oder Delegatkommunikation eine Brücke benötigt. Apple dokumentiert Erstellung, Aktualisierung, Koordination und Abbau als Bestandteile dieser Integration. Apples Dokumentation zu NSViewRepresentable.
Ein Overlay, das Mausereignisse empfängt, kann ändern, welche Ansicht den Klick verarbeitet. AppKit verwendet Trefferprüfung, um den Empfänger von Maus-Down-Ereignissen zu bestimmen. Ein Overlay, das Ereignisse abfängt, sollte daher nicht als unsichtbarer Beobachter behandelt werden. Behalte die Erkennung für diese Aufgabe im auswählbaren Container oder in der eigenen Oberfläche, die tatsächlich für die Interaktion mit dem Element zuständig ist. Apples Dokumentation zur Trefferprüfung.
Auswahl und Öffnen als getrennte Ergebnisse überprüfen
Führe nach dem Erstellen des Beispiels diese Schritte aus:
- Klicke einmal auf
Circle A. Die Zeile sollte ausgewählt werden, die Auswahlbeschriftung sollte den Namen anzeigen und es sollte kein Blatt erscheinen. - Klicke zweimal auf
Circle B. Prüfe, ob das DetailblattCircle Banzeigt. - Schließe das Blatt und klicke einmal auf
Circle C. Prüfe, ob sich die Auswahl ändert, ohne ein weiteres Blatt zu öffnen. - Aktiviere die sichtbare Schaltfläche
Open. Prüfe, ob sie das ausgewählte Element über denselben Handler öffnet. - Rufe den Kontextmenübefehl
Openeiner Zeile auf und prüfe, welches Element dadurch geöffnet wird. - Klicke einmal und doppelt auf ungenutzten Listenbereich. Prüfe, ob dadurch kein anderes Element geöffnet wird.
- Wenn du eingebettete Steuerelemente, Ziehen oder Overlays hinzufügst, wiederhole die Prüfungen genau über diesen Bereichen.
Wenn die Auswahl unerwartet Details öffnet, prüfe, wo der Präsentationszustand gesetzt wird. Wenn bei der Aktivierung das falsche Element geöffnet wird, prüfe die Callback-IDs und die Suche im Modell. Wenn bei einer eigenen Kachel Klicks in einem Teil des Bereichs nicht erkannt werden, prüfe zuerst ihr Layout und ihre Trefferprüfungsform, bevor du die Vorrangigkeit von Gesten änderst. So lassen sich Zustand, Aktionsziel und Ereigniszustellung einzeln untersuchen, statt alle drei gleichzeitig zu ändern.
Halte die Implementierung für eine herkömmliche Mac-Elementliste auf die Auswahlbindung und die primäre Aktion des Containers ausgerichtet. Füge eigene Erkennung nur hinzu, wenn die Oberfläche oder Interaktion sie erfordert, und löse das vorgesehene Element bei jedem Öffnungspfad über einen einzigen expliziten Handler auf.
Quellen
- contextMenu(forSelectionType:menu:primaryAction:) — Apple
- List — Apple
- Identifiable — Apple
- sheet(item:onDismiss:content:) — Apple
- onTapGesture(count:perform:) — Apple
- exclusively(before:) — Apple
- TapGesture — Apple
- contentShape(_:eoFill:) — Apple
- doubleAction — Apple
- mouseDown(with:) — Apple
- clickCount — Apple
- NSViewRepresentable — Apple
- hitTest(_:) — Apple