För en objektlista på Mac kopplar du markeringen till en SwiftUI List och anger en primär åtgärd via dess markeringsmedvetna kontextmeny. Ett klick markerar raden; ett dubbelklick öppnar den. Håll markeringstillståndet åtskilt från visningstillståndet.

Den här handledningen implementerar interaktionen i din egen macOS-app med tre syntetiska objekt som lagras i minnet. Den förklarar också avgränsningarna för anpassade SwiftUI-ytor och AppKit-vyer, så att du kan välja ett tillvägagångssätt utan att flera lager konkurrerar om samma klick.

Använd den markeringsbara behållarens primära åtgärd

Apple dokumenterar uttryckligen markering med ett klick och primära åtgärder med dubbelklick för markeringsbara behållare i macOS. Den SwiftUI-ingångspunkt som stöds är contextMenu(forSelectionType:menu:primaryAction:), tillgänglig från och med macOS 13. Lägg till den på en behållare som stöder markering, till exempel List eller Table. Apples dokumentation om markeringsmedvetna kontextmenyer.

Det finns tre separata ansvarsområden: modellen identifierar varje objekt, listan hanterar markering via en bindning och din hanterare för primära åtgärder avgör vad det innebär att öppna. I det här exemplet visas ett objektinformationsblad när ett objekt öppnas. När ett objekt markeras ändras bara den markerade raden och markeringsetiketten.

Välj implementation utifrån vilken vy som hanterar interaktionen:

Ditt gränssnitt Lämplig utgångspunkt Ansvar du behåller
En SwiftUI-objektlista eller tabell i macOS 13 eller senare Markeringsbindning plus markeringsmedveten primär åtgärd Slå upp åtgärds-ID:n och visa motsvarande innehåll
En anpassad SwiftUI-ruta eller form Uttrycklig gestkomposition Markeringsutseende, aktiveringspolicy och andra inmatningsvägar
En AppKit-tabell inbäddad i SwiftUI NSTableView-markering och doubleAction Överbrygga markering och aktivering till SwiftUI-tillstånd
En anpassad AppKit-rityta Egen hantering av mushändelser Träfftestning, objektidentitet och policy för klicksekvenser

Den första vägen motsvarar uppgiften här direkt. De anpassade vägarna är användbara när gränssnittet verkligen behöver en annan yta; då måste du också implementera mer av interaktionsbeteendet. Apple dokumenterar de underliggande alternativen i gestkomposition, dubbelåtgärder i AppKit-tabeller och hantering av musnedtryckning.

Skapa ett komplett exempel med enkelmarkering

Skapa ett macOS-approjekt i Xcode med ett SwiftUI-gränssnitt och ange macOS 13 eller senare som distributionsmål. Ersätt de genererade deklarationerna för appen och innehållsvyn med följande enda fil. Ha bara en @main-appdeklaration i målet.

Programmet använder fasta heltals-ID:n, tre färgade cirklar och lokal tillståndshantering. Det läser inga filer och öppnar inga externa resurser. Verktygsfältsknappen Open och kommandot Open i kontextmenyn anropar samma funktion som aktivering med dubbelklick.

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

Deklarationerna och referenserna har granskats, men exemplet har inte kompilerats eller körts här: den tillgängliga miljön saknar Swift-verktygskedja och macOS-GUI. Bygg det i Xcode och utför interaktionskontrollerna nedan.

Håll identitet, markering och öppning åtskilda

SampleItem följer Swifts Identifiable-protokoll. Dess fasta id representerar samma objekt mellan vyuppdateringar. Apple definierar det här protokollet utifrån stabil identitet; visningsnamnet är en separat egenskap. I en större app bör ett objekts ID bevaras även om dess namn eller utseende ändras. Apples dokumentation för Identifiable.

Det valfria selectedID skapar en lista med enkelmarkering. En mängd ID:n skulle i stället aktivera flermarkering. Swifts dokumentation för listor beskriver de här två bindningsformerna och markeringens utseende som behållaren tillhandahåller. Apples dokumentation för List.

openedItem styr visningen separat. Om värdet sätts till ett objekt visas informationsbladet; om det sätts till nil stängs det. Det följer det dokumenterade objektstyrda API:t för informationsblad. Eftersom en radmarkering aldrig tilldelar openedItem kan markeringen ensam inte visa informationsbladet i den här implementationen. Apples dokumentation för objektstyrda informationsblad.

Slå upp ID:n som skickas till åtgärden

API:t för kontextmenyer använder behållarens identifierartyp, och åtgärden får en mängd ID:n. Därför använder exemplet SampleItem.ID.self, vilket motsvarar listans markering. Det slår upp argumentet från återanropet i stället för att ersätta det med ett separat avläst markeringsvärde. Apples dokumentation om parametrar för kontextmenyer.

Hanteraren tar avsiktligt emot exakt ett ID. En tom mängd ger ett meddelande, och ett ID som inte kan slås upp ger ett annat meddelande. Om du senare aktiverar flermarkering bör du fastställa policyn för öppning innan du ändrar den här kontrollen: visa en gruppdetaljvy, öppna separata fönster eller kräv en enda markering. Om du godtyckligt väljer den första medlemmen döljer du en odefinierad policy.

Varför två tryckhanterare kräver ett annat designbeslut

En anpassad ruta kan behöva egna gester. SwiftUI:s onTapGesture(count:perform:) tar emot ett obligatoriskt antal klick eller tryck. Dokumentationen fastslår dock inte att två separata modifierare på en vy skapar ett exklusivt kontrakt för enkel- respektive dubbelklick. Apples dokumentation för tryckåtgärder.

Bestäm först vad markering innebär vid ett dubbelklick. För en vanlig objektbläddrare är det användbart och konsekvent med lösningen som bygger på en behållare att markera objektet innan det öppnas. Ett separat krav – att utföra enkelklicksåtgärden endast när sekvensen inte blir ett dubbelklick – kräver samordning.

För en anpassad yta tillhandahåller SwiftUI exclusively(before:): bara en av komponentgesterna lyckas, med företräde för den första. Följande är ett gestuttryck, inte en komplett ersättning för listexemplet: Apples dokumentation om exklusiva gester.

TapGesture(count: 2)
    .exclusively(before: TapGesture(count: 1))

Det här sätter igenkänning av dubbelklick först. Lägg till den sammansatta gesten på det anpassade objektet och hantera resultatet enligt din policy för markering och öppning. TapGesture dokumenterar igenkänning utifrån det obligatoriska antalet tryck. Apples dokumentation för TapGesture.

Den policyn får en tidsmässig konsekvens: för att avgöra att en sekvens uteslutande är ett enkelklick måste man utesluta ett dubbelklick. Det är en följd av interaktionskravet, inte en dokumenterad, fast fördröjning i SwiftUI. Undvik att utlova omedelbar hantering av enkelklick samtidigt som du kräver att det aldrig inträffar under en dubbelklickssekvens.

Definiera också den avsedda klickbara ytan på en anpassad ruta. contentShape(Rectangle()) kan få ett utfyllt rektangulärt område att delta i träfftestningen; det avgör inte gestprioritet. Håll isär de här två frågorna när du felsöker missade klick. Apples dokumentation om innehållsformer.

Behåll händelsehanteringens ägarskap när AppKit används

Om en befintlig AppKit-tabell hanterar raderna använder du tabellens stödda aktiveringsmekanism. NSTableView.doubleAction skickar en selektor till sitt mål vid dubbelklick. Apple dokumenterar också clickedRow och clickedColumn för att avgöra var aktiveringen skedde, inklusive kolumnrubriker. Kontrollera att platsen identifierar ett objekt innan du öppnar det. Apples dokumentation för doubleAction.

För en anpassad AppKit-yta tar mouseDown(with:) emot en händelse när vänster musknapp trycks ned, och NSEvent.clickCount rapporterar antalet klick som hör till händelser när musknappen trycks ned eller släpps upp. Att läsa den egenskapen för en orelaterad händelse ligger utanför det dokumenterade kontraktet och kan orsaka ett undantag. Begränsa sådan tolkning till vyn som hanterar rätt mushändelse. Apples dokumentation om musnedtryckning, Apples dokumentation om klickantal.

Använd NSViewRepresentable när du bäddar in den AppKit-vyn i SwiftUI. Skapa den i makeNSView, synkronisera den aktuella konfigurationen i updateNSView och använd en koordinator när målåtgärder eller delegatkommunikation behöver en brygga. Apple beskriver skapande, uppdatering, koordinering och nedmontering som delar av den här integrationen. Apples dokumentation för NSViewRepresentable.

Ett överlägg som tar emot mushändelser kan ändra vilken vy som hanterar klicket. AppKit använder träfftestning för att avgöra vilken vy som tar emot händelser när musknappen trycks ned, så ett händelsefångande överlägg bör inte behandlas som en osynlig observatör. I den här uppgiften bör igenkänningen ligga i den markeringsbara behållaren eller den anpassade yta som faktiskt hanterar objektinteraktionen. Apples dokumentation om träfftestning.

Kontrollera markering och öppning som separata resultat

Utför följande steg när du har byggt exemplet:

  1. Klicka en gång på Circle A. Raden ska markeras, markeringsetiketten ska visa dess namn och inget informationsblad ska visas.
  2. Dubbelklicka på Circle B. Kontrollera att informationsbladet visar Circle B.
  3. Stäng informationsbladet och klicka en gång på Circle C. Kontrollera att markeringen ändras utan att ett nytt informationsblad öppnas.
  4. Aktivera den synliga knappen Open. Kontrollera att det markerade objektet öppnas via samma hanterare.
  5. Anropa kommandot Open i en rads kontextmeny och kontrollera vilket objekt det öppnar.
  6. Klicka och dubbelklicka på oanvänt listutrymme. Kontrollera att inget orelaterat objekt öppnas.
  7. Om du lägger till inbäddade kontroller, dragning eller överlägg upprepar du kontrollerna över just de områdena.

Om markering oväntat öppnar objektinformation bör du kontrollera var visningstillståndet tilldelas. Om aktivering öppnar fel objekt bör du kontrollera ID:n från återanropet och modellsökningen. Om klick missas på delar av en anpassad ruta bör du kontrollera dess layout och träfftestningsform innan du ändrar gestprioriteten. Kontrollerna isolerar tillstånd, val av åtgärdsmål och händelseleverans i stället för att ändra allt på en gång.

För en vanlig objektlista på Mac bör implementationen centreras kring markeringsbindningen och behållarens primära åtgärd. Lägg bara till anpassad igenkänning när ytan eller interaktionen kräver det, och se till att varje öppningsväg slår upp det avsedda objektet via en tydlig hanterare.

Källor