Koppel de sleepactie van de ouder met simultaneousGesture(_:including:) om toe te staan dat deze meedoet naast kindinteracties. Houd klikacties in kindknoppen, geef slepen een drempelwaarde voor beweging, en besluit expliciet of selectie mag meegaan met een sleepactie.

Deze walkthrough bouwt een verplaatsbaar paneel met drie synthetische itembuttons. De taak voor de lezer is specifiek: klikken op een item selecteert het, terwijl het slepen van het paneel de positie ervan verandert binnen dezelfde Mac-app. Het voorbeeld laat selectie en verplaatsing coexisteren; een aparte handvat-variatie biedt afzonderlijke interactieregio's wanneer dat gewenst is.

Kies het interactiecontract voordat je de gebarenprioriteit wijzigt

Apple documenteert simultaneousGesture(_:including:) als het toevoegen van een gebaar met dezelfde prioriteit als bestaande gebaren. Het voorbeeld toont een kind-tap-handler en een tap-handler van de omringende container die allebei reageren op een klik. Dit is het relevante compositiemechanisme wanneer de ouder moet deelnemen zonder bewust prioriteit te claimen boven het kind. Apple's documentatie over gelijktijdige gebaren

Gelijktijdige herkenning drukt echter niet zelf de regel uit dat "slepen elke kindactie moet annuleren." Noch stelt Apple's voorbeeld vast dat elke buttonstijl en elke container een button en een sleepactie identiek bemiddelt. Behandel gebaarparticipatie en toepassingsgedrag als afzonderlijke beslissingen.

Voor dit paneel bezit de parent de paneel-offset en het geselecteerde item. Elk child bezit een knop die selectie aanvraagt via een closure. Een selectie-aanvraag blijft acceptabel, zelfs als dezelfde pointer-interactie het paneel verplaatst. Als uw gewenste gedrag activering tijdens verplaatsing verbiedt, gebruik dan de afzonderlijke handle-variant hieronder in plaats van te vertrouwen op ongedocumenteerde callback-volgorde.

Gebruik een Button voor de child-actie. Apple beveelt expliciet knoppen aan wanneer een interactie knopgedrag vereist, inclusief standaard toegankelijkheidsgedrag. Een aangepaste vorm kan het label van de knop blijven; het hoeft niet een losse tap-gebaar te worden. Apple’s Gestures-documentatie

Bouw een compleet verplaatsbaar paneel met child-knoppen

Maak een macOS SwiftUI-app in Xcode, stel het implementatiedoel in op macOS 14 of later, en vervang de gegenereerde app en content view door het volgende enkele bestand. Houd slechts één app-toegangspunt in het doel. De macOS 14-vereiste dekt de moderne coordinateSpace(_:)-modifier die hier gebruikt wordt. Apple’s coördinateruimte-documentatie

Alle items en state bestaan in het geheugen. Het voorbeeld verplaatst het hele paneel, inclusief de knoppen, binnen een vast stadium. Het implementeert geen itemoverdracht en verplaatst niet het applicatievenster.

import SwiftUI

@main
struct GesturePanelApp: App {
    var body: some Scene {
        WindowGroup {
            PanelDemo()
        }
    }
}

private struct DemoItem: Identifiable {
    let id: Int
    let title: String
    let color: Color
}

private struct ItemTarget: View {
    let item: DemoItem
    let isSelected: Bool
    let select: () -> Void

    var body: some View {
        Button(action: select) {
            HStack(spacing: 10) {
                Circle()
                    .fill(item.color)
                    .frame(width: 18, height: 18)
                Text(item.title)
                Spacer()
                if isSelected {
                    Text("Selected")
                        .font(.caption)
                }
            }
            .padding(12)
            .frame(width: 220)
            .background(
                isSelected
                    ? Color.accentColor.opacity(0.18)
                    : Color.gray.opacity(0.12)
            )
            .contentShape(Rectangle())
        }
        .buttonStyle(.plain)
    }
}

private struct PanelDemo: View {
    private let items = [
        DemoItem(id: 1, title: "Item A", color: .blue),
        DemoItem(id: 2, title: "Item B", color: .orange),
        DemoItem(id: 3, title: "Item C", color: .purple)
    ]

    @State private var selectedID: Int? = nil
    @State private var committedOffset: CGSize = .zero
    @GestureState private var liveTranslation: CGSize = .zero

    private var pan: some Gesture {
        DragGesture(
            minimumDistance: 8,
            coordinateSpace: .named("stage")
        )
        .updating($liveTranslation) { value, translation, _ in
            translation = value.translation
        }
        .onEnded { value in
            committedOffset = CGSize(
                width: committedOffset.width + value.translation.width,
                height: committedOffset.height + value.translation.height
            )
        }
    }

    var body: some View {
        VStack(spacing: 12) {
            Text("Click an item or drag the panel")

            ZStack {
                Color.gray.opacity(0.06)

                VStack(spacing: 10) {
                    Text("Drag panel")
                        .font(.headline)
                        .frame(width: 244, height: 32)
                        .contentShape(Rectangle())

                    ForEach(items) { item in
                        ItemTarget(
                            item: item,
                            isSelected: selectedID == item.id,
                            select: { selectedID = item.id }
                        )
                    }
                }
                .padding(16)
                .background(Color.gray.opacity(0.15))
                .contentShape(Rectangle())
                .simultaneousGesture(pan, including: .all)
                .offset(
                    x: committedOffset.width + liveTranslation.width,
                    y: committedOffset.height + liveTranslation.height
                )
            }
            .frame(width: 640, height: 420)
            .coordinateSpace(.named("stage"))

            Button("Reset panel") {
                committedOffset = .zero
            }
        }
        .padding(20)
    }
}

Dit programma is geïnspecteerd op basis van de opgehaalde API-documentatie, maar is hier niet gecompileerd of interactief getest: deze omgeving heeft geen Swift-compiler of macOS UI-runtime. Bouw het in Xcode en voer de onderstaande controles uit op de macOS-versies die u ondersteunt.

Waarom de sleepactie een niet-nul-drempel heeft

DragGesture geeft een minimale verplaatsingsafstand aan voordat de gebaar herkend wordt. De illustratieve waarde 8 geeft gewone klikken een bepaalde verplaatsingstolerantie voordat het parent-element begint te slepen. Het is een toepassingskeuze, geen door Apple voorgeschreven drempel of een compleet klik-versus-sleepbeleid. Een nul-afstand sleepactie verwijdert die specifieke onderscheiding. Apple's DragGesture-documentatie

Waarom tijdelijke en vastgelegde verplaatsing gescheiden zijn

Tijdens herkenning schrijft updating(_:body:) de huidige vertaling naar liveTranslation. Apple documenteert dat deze gebaarstatus wordt gereset wanneer de gebaar eindigt of geannuleerd wordt. Het voorbeeld legt de definitieve offset alleen vast in onEnded, zodat annulering de niet-vaste voorvertoning verwijdert in plaats van een verouderde tijdelijke vertaling achter te laten. Apple's updating-documentatie

De weergegeven positie is de vastgelegde offset plus de huidige vertaling. Apple definieert translation als totale verplaatsing vanaf het begin van de sleepactie, dus het herhaaldelijk toevoegen ervan aan de vastgelegde offset bij elke verandering zou de beweging dubbel tellen. Ter illustratie: een vastgelegde horizontale offset van 40 en een huidige vertaling van 12 produceren een weergegeven offset van 52; het voltooien van die sleepactie vastlegt 52 één keer. Apple’s translation documentation

De genoemde coördinateruimte behoort tot de vaste podium die het bewegende paneel omringt. Hierdoor verwijst de bewegingsberekening naar een expliciete referentieregio. De kindknoppen onderhouden geen eigen kopieën van de paneelpositie.

Waarom selectie bij de bovenliggende blijft

De bovenliggende slaat selectedID op en voorziet elk kind van de huidige geselecteerde status en een selectie-closure. Dit volgt Apple's richtlijn om state in de hoogste view te houden die het nodig heeft, en vervolgens waarden of bindings door de hiërarchie door te geven. Apple’s State documentation

De closure vangt het huidige item op en verzoekt een update van de state van de bovenliggende. Swift ondersteunt closures die waarden uit hun omringende context vangen; geen AppKit-event hoeft gekopieerd of doorgestuurd te worden om die callback te implementeren. The Swift Programming Language: Closures

Raakgebieden en gebarenmaskers begrijpen

contentShape(Rectangle()) definieert de hit-test-vorm. In de knoplabel volgt het de padding en frame, zodat de bedoelde rechthoekige labelregio expliciet is. Op de ouder definieert het de interactievorm van het paneel. Het stelt geen herkenningsprioriteit in en vereist niet dat twee gebaren samenwerken. Apple’s content shape documentation

De mask die aan een gebarenmodifier wordt meegegeven, bepaalt welke gebaren ingeschakeld blijven. Deze opties zijn gemakkelijk verkeerd te lezen: subviews betekent dat het toegevoegde ouergebaar uitgeschakeld is, niet dat het automatisch samenwerkt met nakomelingen. Apple’s GestureMask documentation

Mask Gedocumenteerd effect Geschiktheid hier
.all Schakelt het toegevoegde gebaar en andere gebaren op de view en zijn subviews in Startpunt voor gedeelde participatie
.gesture Schakelt het toegevoegde gebaar in en schakelt gebaren in de subview-hiërarchie uit In conflict met het behouden van kindgebaren
.subviews Schakelt subview-gebaren in en schakelt het toegevoegde gebaar uit Schakelt de ouer-sleepactie uit
.none Schakelt gebaren in de subview-hiërarchie uit, inclusief het toegevoegde gebaar Schakelt de bedoelde interactie uit

Vermijd het behandelen van highPriorityGesture als een algemene oplossing voor ontbrekende parent-drag-callbacks. Het gedocumenteerde doel is om de gekoppelde gebaar een hogere prioriteit te geven. Apples voorbeeld toont de parent die prioriteit krijgt boven een child-tap. Kies dit wanneer die prioriteit het beoogde gedrag is. Apple-documentatie over prioritaire gebaren

Gebruik een aparte drag-handle wanneer activering apart moet blijven

Voor een paneel waarvan de knoppen geen gedeelde drag-regio mogen hebben, behoud dezelfde staat en bewegingsberekening, maar koppel pan alleen aan de header. Verwijder .simultaneousGesture(pan, including: .all) uit het parent-paneel.

Vervang de bestaande header-expressie door dit fragment binnen het complete programma hierboven. Het gebruikt de al gedefinieerde eigenschap pan; geen andere vereisten veranderen.

Text("Drag panel")
    .font(.headline)
    .frame(width: 244, height: 32)
    .contentShape(Rectangle())
    .gesture(pan)

De header en item-knoppen bezetten nu afzonderlijke regio's, en de knoppen hebben de paneel-drag niet meer gekoppeld aan een voorouder. Starten op een item is een knop-interactie; starten op de header is de weg om het paneel te verplaatsen. Dit ontwerp is nuttig wanneer gedeelde participatie het resultaat ambigu zou maken.

Behoud de native button wanneer het kind een actie initieert. Apple beschrijft Button als een actie plus een label en staat aangepaste label-weergaven toe, zoals gebruikt door de synthetische gekleurde items. Het vervangen ervan door een handmatig gebouwde pointer-handler zou vereisen dat je gedrag opnieuw creëert dat de control al biedt. Apple's Button-documentatie

Behoud AppKit-gebeurteniseigendom aan frameworkgrenzen

Als het paneel een AppKit-weergave bevat, diagnoseer de hit-testing ervan apart van de SwiftUI-gebaarcompositie. AppKit's hitTest(_:) retourneert de diepste overeenkomende nakomeling en helpt het venster de ontvanger van een muis-neer-gebeurtenheid te bepalen. De invoerpunt wordt uitgedrukt in de superview-coördinaten van de weergave. Apple's AppKit hit-testing-documentatie

Het retourneren van een parent uit hit-testing verandert het initiële doel; het is niet equivalent aan het toestaan dat een kind en parent tegelijkertijd herkennen. Op dezelfde manier verwijdert het retourneren van nil uit de hit-test van een overlay die overlay uit overweging—het maakt de overlay niet tot een passieve waarnemer van de complete interactie van het kind.

AppKit documenteert dat de standaard mouseDown(with:) implementatie het bericht doorgeeft aan de volgende responder. Dat is responder-keten gedrag, geen belofte dat elke voorouder onafhankelijk een kopie ontvangt nadat een control het event heeft verwerkt. Houd control-activering en paneelverplaatsing verbonden via expliciete callbacks of recognizers die je zelf beheert. Apple’s mouse-down documentation

Als je twee AppKit-gebarenrecognizers bezit en gelijktijdige herkenning nodig hebt, biedt AppKit gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:). Apple documenteert dat het teruggeven van true het paar toestaat om samen te herkennen; het teruggeven van false garandeert geen uitsluiting als de delegate van de andere recognizer dit toestaat. Beperk dat beleid tot de beoogde recognizer-koppel. Apple’s recognizer delegate documentation

Controleer de interactie met een kleine gedragsmatrix

Test de exacte knopstijl, hiërarchie en macOS-versies die je app ondersteunt. De volgende controles scheiden herkeningsproblemen van beweegrekenkunde en verduidelijken of het gedeelde oppervlak overeenkomt met je beoogde contract.

Actie Wat te controleren
Klik op elk item zonder te slepen De geselecteerde status verandert; het paneel blijft stil
Sleep de header voorbij de drempel Het paneel volgt de wijzer en behoudt zijn eindpositie
Sleep vanaf een item in de gedeelde versie De ouderbeweging werkt; vaststellen of knopactivering ook plaatsvindt en bevestigen dat dit acceptabel is
Voer twee opeenvolgende sleeppogingen uit De tweede begint vanaf de eerste vastgelegde positie zonder elke tussentijdse translatie op te tellen
Onderbreek een actieve sleepactie Tijdelijke translatie wordt gewist; geen onvoltooide beweging wordt vastgelegd
Probeer de handle-variatie Items activeren onafhankelijk; beweging begint vanaf de header
Activeer itemknoppen via toetsenbordnavigatie Selectie werkt zonder dat een wijzer-sleepactie vereist is

Als leeg paneelruimte correct sleept maar itemregio's niet, inspecteer onderliggende controls en gestoedsamenstelling. Als klikken verdwijnen, inspecteer gestoedmaskers, prioriteit en hit-regio's. Als het paneel versnelt of springt, inspecteer translatie-accumatie en de gekozen coördinateruimte voordat je meer recognizers toevoegt.

Het voorbeeld laat de beweging bewust onbegrensd, zodat het paneel verder kan bewegen dan het zichtbare podium; de Reset panel-knop herstelt de positie. Voor een afgeronde begrensde werkruimte, voeg een expliciete positiebeperking toe gebaseerd op de podium- en paneelafmetingen. Behoud dezelfde eigenaarsstructuur: kinderen verzoeken acties, de ouder bezit de beweging, en het interactiecontract bepaalt of hun herkenningregio's overlappen.

Bronnen