Anslut förälderns drag med simultaneousGesture(_:including:) för att tillåta att det deltar tillsammans med barninteraktioner. Behåll klickåtgärder i barnknappar, ge draggning en rörelsetröskel, och bestäm explicit om urval kan åtfölja ett drag.
Den här genomgången bygger en flyttbar panel som innehåller tre syntetiska artikelknappar. Läsarens uppgift är specifik: att klicka på en artikel väljer den, medan att dra panelen ändrar dess position i samma Mac-app. Exemplet tillåter urval och flyttning att samförekomma; en separat handtagsvariation ger distinkta interaktionsregioner när det är att föredra.
Välj interaktionskontrakt innan du ändrar gestens prioritet
Apple dokumenterar simultaneousGesture(_:including:) som att lägga till en gest med samma prioritet som befintliga gesters. Dess exempel visar en barn-tap-handler och en omgivande containers tap-handler som båda svarar på ett klick. Det är den relevanta sammansättningsmekanismen när föräldern ska delta utan att avsiktligt ta precedens över barnet. Apples dokumentation om samtidiga gesters
Men samtidig igenkänning uttrycker inte själva regeln "draggning måste avbryta varje barn-åtgärd." Etset heller etablerar Apples exempel att varje knappstil och varje container skiljer en knapp och ett drag identiskt. Behandla gestdeltagande och applikationsbeteende som separata beslut.
För den här panelen äger föräldern paneloffset och valt objekt. Varje barn äger en knapp som begär urval genom en closure. En urvalsförfrågan förblir acceptabel även om samma pekareinteraktion flyttar panelen. Om ditt krävda beteende förbjuder aktivering under rörelse, använd den separata handtagsvariationen nedan istället för att lita på odokumenterad callback-ordning.
Använd en Button för barnåtgärden. Apple rekommenderar explicit knappar när en interaktion behöver knappbeteende, inklusive standard tillgänglighetsbeteende. En anpassad form kan förbli knappens etikett; den behöver inte bli en bar tap-gest. Apples Gestures-dokumentation
Bygg en komplett flyttbar panel med barnknappar
Skapa en macOS SwiftUI-app i Xcode, ställ in dess distributionsmål på macOS 14 eller senare, och ersätt den genererade appen och innehållsvyn med följande enda fil. Behåll endast en app-startpunkt i målet. Kravet på macOS 14 täcker den moderna coordinateSpace(_:)-modifieraren som används här. Apples koordinatutrymmesdokumentation
Alla objekt och tillstånd finns i minnet. Exemplet flyttar hela panelen, inklusive dess knappar, inom en fast scen. Det implementerar inte artikelöverföring eller flyttar applikationsfönstret.
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)
}
}
Det här programmet har granskats mot den hämtade API-dokumentationen, men det har inte kompilerats eller interaktivt testats här: den här miljön har ingen Swift-kompilator eller macOS UI-körning. Bygg det i Xcode och utför kontrollerna nedan på de macOS-versioner du stöder.
Varför draget har en nollskild tröskel
DragGesture visar ett minimalt rörelseavstånd innan gesten lyckas. Det illustrativa värdet 8 ger vanliga klick viss rörelsetolerans innan föräldern börjar dra. Det är ett applikationsval, inte en Apple-förordnad tröskel eller en komplett klick-mot-drag-politik. Ett nollavstånds-drag tar bort den specifika skillnaden. Apples DragGesture-dokumentation
Varför tillfällig och fastställd rörelse är separata
Under igenkänning skriver updating(_:body:) den aktuella översättningen till liveTranslation. Apple dokumenterar att detta gesttillstånd återställs när gesten avslutas eller avbryts. Exemplet fastställer det slutgiltiga offsetet först i onEnded, så avbryt kasserar den opåfastställda förhandsvisningen istället för att lämna en föråldrad tillfällig översättning. Apples updating-dokumentation
Den visade positionen är det fastställda offsetet plus den aktuella översättningen. Apple definierar translation som total förskjutning från dragets start, så att lägga till det upprepade till det fastställda offsetet vid varje ändring skulle räkna rörelsen dubbelt. Som illustration ger ett fastställt horisontellt offset på 40 och en aktuell översättning på 12 en visad offset på 52; att slutföra det draget fastställer 52 en gång. Apples translation-dokumentation
Det namngivna koordinatutrymmet tillhör den fasta scenen som omger den rörliga panelen. Detta gör att rörelseberäkningen refererar till en explicit referensregion. Barnknapparna håller inte sina egna kopior av panelpositionen.
Varför urvalet stannar hos föräldern
Föräldern lagrar selectedID och tillhandahåller varje barn med dess nuvalda urvalsstatus och en urvals-closure. Detta följer Apples vägledning att hålla tillstånd i den högsta vy som behöver det, och sedan skicka värden eller bindings ner i hierarkin. Apples State-dokumentation
Closuren fångar det aktuella objektet och begär en uppdatering av förälderns tillstånd. Swift stödjer closures som fångar värden från sin omgivande kontext; ingen AppKit-händelse behöver kopieras eller vidarebefordras för att implementera den callbacken. The Swift Programming Language: Closures
Förstå träffregioner och gestmasker
contentShape(Rectangle()) definierar träfftestningsformen. I knappens etikett följer den padding och frame så att den avsedda rektangulära etikettregionen är explicit. På föräldern definierar den panelens interaktionsform. Den sätter inte igenkännarens prioritet eller kräver att två gesters samarbetar. Apples content shape-dokumentation
Masken som tillhandahålls till en gest-modifierare avgör vilka gesters som förblir aktiverade. Dessa alternativ är lätt att missläsa: subviews betyder att den tillagda föräldergesten är inaktiverad, inte att den automatiskt samarbetar med efterkommande. Apples GestureMask-dokumentation
| Mask | Dokumenterad effekt | Lämplighet här |
|---|---|---|
.all |
Aktiverar den tillagda gesten och andra gesters på vyn och dess undervyer | Startpunkt för delat deltagande |
.gesture |
Aktiverar den tillagda gesten och inaktiverar gesters i undervy-hierarkin | Strider mot att bevara barn-gesters |
.subviews |
Aktiverar undervy-gestern och inaktiverar den tillagda gesten | Inaktiverar förälderdraget |
.none |
Inaktiverar gesters i undervy-hierarkin, inklusive den tillagda gesten | Inaktiverar den avsedda interaktionen |
Undvik att behandla highPriorityGesture som en generell reparation för saknade förälderdrag-callbacks. Dess dokumenterade syfte är att ge den anslutna gesten högre precedens. Apples exempel visar föräldern ta precedens över en barn-tap. Välj det när den precedensen är det avsedda beteendet. Apples high-priority gesture-dokumentation
Använd ett separat draghandtag när aktivering måste vara distinkt
För en panel vars knappar inte ska ha en delad dragregion, behåll samma tillstånd och rörelseberäkning men anslut pan endast till rubriken. Ta bort .simultaneousGesture(pan, including: .all) från förälderpanelen.
Ersätt det befintliga rubrikuttrycket med detta fragment inuti det fullständiga programmet ovan. Det använder den redan definierade pan-egenskapen; inga andra förutsättningar ändras.
Text("Drag panel")
.font(.headline)
.frame(width: 244, height: 32)
.contentShape(Rectangle())
.gesture(pan)
Rubriken och artikelknapparna upptar nu distinkta regioner, och knapparna har inte längre paneldraget anslutet till en förfader. Att starta på en artikel är en knappinteraktion; att starta på rubriken är vägen till att flytta panelen. Den här designen är användbar när delat deltagande skulle göra resultatet tvetydigt.
Behåll den inbyggda knappen när barnet initierar en åtgärd. Apple beskriver Button som en åtgärd plus en etikett och tillåter anpassade etikettvyer, som används av de syntetiska färgade artiklarna. Att ersätta det med en handbyggd pekarehanterare skulle kräva att du återskapar beteendet som kontrollen redan levererar. Apples Button-dokumentation
Behåll AppKit-händelseägande vid ramverksgränser
Om panelen innehåller en AppKit-vy, diagnostisera dess träfftestning separat från SwiftUI-gestkomposition. AppKits hitTest(_:) returnerar den djupaste matchande efterkommande och hjälper fönstret att bestämma mottagaren av en musned-händelse. Dess inmatningspunkt uttrycks i vyns superview-koordinater. Apples AppKit träfftestningsdokumentation
Att returnera en förälder från träfftestning ändrar det initiala målet; det är inte ekvivalenten med att låta ett barn och en förälder känna igen samtidigt. Likaså tar bort retur av nil från en overlays träfftest det överlagret från överväganden — det gör inte att överlagret blir en passiv observatör av barnets fullständiga interaktion.
AppKit dokumenterar att standardimplementeringen av mouseDown(with:) skickar meddelandet till nästa responder. Det är responder-kedja-beteende, inte ett löfte om att varje förfader oberoende får en kopia efter att en kontroll hanterat händelsen. Håll kontrollaktivering och panelflytt kopplade genom explicita callbacks eller igenkännare du äger. Apples mouse-down-dokumentation
När du äger två AppKit-gestigenkännare och behöver samtidig igenkänning, tillhandahåller AppKit gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:). Apple dokumenterar att retur av true tillåter att paret känner igen tillsammans; retur av false garanterar inte uteslutning om den andra igenkännarens delegate tillåter det. Begränsa den policyn till det avsedda igenkännar-paret. Apples recognizer delegate-dokumentation
Verifiera interaktionen med en liten beteendematris
Testa den exakta knappstilen, hierarkin, och macOS-utgåvorna din app stöder. Följande kontroller skiljer igenkänningsproblem från rörelsearitmetik och klargör om den delade ytan matchar ditt avsedda kontrakt.
| Åtgärd | Vad att kontrollera |
|---|---|
| Klicka på varje artikel utan att dra | Dess urvalstillstånd ändras; panelen står still |
| Dra rubriken förbi tröskeln | Panelen följer pekaren och behåller sin slutliga position |
| Dra från en artikel i den delade versionen | Förälderflytt fungerar; avgör om knappaktivering också inträffar och bekräfta att det är acceptabelt |
| Gör två på varandra följande drag | Det andra startar från det första fastställda läget utan att ackumulera varje mellantilståndsöversättning |
| Avbryt ett aktivt drag | Tillfällig översättning rensas; ingen ofärdig flytt fastställs |
| Prova handtagsvariationen | Artiklar aktiveras oberoende; flytt börjar från rubriken |
| Aktivera artikelknappar via tangentbordsnavigering | Urval fungerar utan att kräva ett pekardrag |
Om tomt panelutrymme drar korrekt men artikelregioner inte gör det, inspektera barnkontroller och gestkomposition. Om klick försvinner, inspektera gestmasker, precedens, och träffregioner. Om panelen accelererar eller hoppar, inspektera översättningsackumulation och det valda koordinatutrymmet innan du lägger till fler igenkännare.
Exemplet lämnar flyttningen avsiktligt obegränsad, så panelen kan flytta bortom den synliga scenen; knappen Reset panel återställer dess position. För en färdig begränsad arbetsyta, lägg till en explicit positionsbegränsning baserad på scenens och panelns dimensioner. Behåll samma ägandestruktur: barn begär åtgärder, föräldern äger flyttningen, och interaktionskontraktet avgör om deras igenkänningsregioner överlappar.
Källor
- simultaneousGesture(_:including:) — Apple
- Gestures — Apple
- coordinateSpace(_:) — Apple
- DragGesture — Apple
- updating(_:body:) — Apple
- translation — Apple
- State — Apple
- Closures — The Swift Programming Language, Swift project
- contentShape(_:eoFill:) — Apple
- GestureMask — Apple
- highPriorityGesture(_:including:) — Apple
- Button — Apple
- hitTest(_:) — Apple
- mouseDown(with:) — Apple
- gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) — Apple