Återställ nedtonade former, konturer och dragetiketter från dragkällans återanropsfunktion när sessionen avslutas. En destinations återanropsfunktion för utträde betyder bara att pekaren lämnade det målet. För en Mac-app använder du SwiftUI:s API för källsessioner när det finns tillgängligt, eller låter en AppKit-källa äga dragningen.

Den här guiden behandlar en uppgift: att återställa en syntetisk form till dess normala utseende när användaren har slutat dra, även när inget mål tar emot den. Exemplet behåller innehållet i minnet och behandlar ett godkänt släpp som en kopia, så återställning av återkopplingen kräver aldrig att källan tas bort eller flyttas.

Varför förblir dragåterkopplingen aktiv utanför släppmål?

En vanlig implementation sätter en isDragging-flagga när dragningen börjar och nollställer den i performDrop. Då kopplas rensningen till en händelse som bara ett mottagande mål kan hantera. Om du släpper över tom yta i fönstret finns inget mål som ansvarar för att återställa källan.

Apple definierar DropDelegate som ett gränssnitt för en vy som har modifierats för att ta emot släpp. Metoden dropExited(info:) rapporterar att en validerad dragning lämnade vyn. Dragningen kan fortsätta till ett annat mål, återvända till det första målet eller avslutas någon annanstans. Därför kan ett målutträde inte fastställa att källsessionen avslutades. DropDelegate

Samma skillnad gäller för dropDestination(for:action:isTargeted:): isTargeted beskriver om pekaren befinner sig inom målområdet. Använd den för målets kant eller bakgrund. Låt en återanropsfunktion för källsessionen styra källans nedtoning eller dragetikett. dropDestination(for:action:isTargeted:)

En användbar designregel följer av dessa dokumenterade gränser: tilldela varje visuellt tillstånd till den händelseägare som på ett tillförlitligt sätt kan avsluta det.

Återkoppling eller resultat Ägare Återställnings- eller bekräftelsepunkt
Nedtonad källform Dragkälla Källsessionen avslutas
Markerad destination Släppdestination Pekaren lämnar området; komplettera med defensiv rensning när en lokal dragning avslutas
Godkänt syntetiskt objekt Destination/modell Släppet valideras och lyckas
Tillfälliga exporterade resurser Överföringslivscykel Överföringen slutförs, när det är tillämpligt

Vilket stödd tillvägagångssätt bör en Mac-app använda?

Apples aktuella referens anger att onDragSessionUpdated(_:) är tillgängligt på macOS 26 och senare. Det observerar uppdateringar av den närmaste dragsessionen i vyns underordnade vyhierarki. Lägg till det direkt utanför källans dragmodifierare så att omfattningen är tydlig. Använd ett SDK som faktiskt deklarerar metoden; tillgänglighet vid körning gör inte att en saknad SDK-deklaration kan kompileras. onDragSessionUpdated(_:)

Det finns en avvikelse i dokumentationen som är värd att hantera snävt: modifierarens exempel visar en avslutad fas med argument för position och åtgärd, medan den särskilda fasreferensen deklarerar ended(DropOperation). Exemplet nedan matchar .ended utan att hämta associerade värden, eftersom visuell återställning inte behöver dem. Kontrollera deklarationen i ditt installerade SDK innan du lägger till åtgärdsspecifikt beteende. DragSession.Phase.ended(_:)

Om ditt distributionsmål eller SDK inte kan använda SwiftUI-återanropet ger en AppKit-dragkälla en dokumenterad gräns för slutförande. Apple anger uttryckligen att en källa får draggingSession(_:endedAt:operation:) när dess dragning avslutas eller avbryts. En anpassad källa måste själv initiera sessionen. beginDraggingSession(with:event:source:)

Om du bara behöver flytta en form inom en enda arbetsyta kan du överväga en lokal DragGesture i stället för en systemsession för dra och släpp. @GestureState återställs när gesten blir inaktiv, vilket gör den lämplig för gestens tillfälliga återkoppling. Håll det valet åtskilt från en systemdragning som tillhandahåller överförbart innehåll till destinationer. GestureState

Implementera källägd rensning i SwiftUI

Skapa en macOS-app med SwiftUI med ett SDK som exponerar onDragSessionUpdated(_:), ange macOS 26 eller senare som distributionsmål och ersätt de genererade app- och innehållsvyedeklarationerna med det här kompletta programmet. Det tillhandahåller en syntetisk sträng, shape.circle, och ett mottagningsområde.

Källan använder draggable(_:) för att tillhandahålla överförbart innehåll. Destinationen godkänner exakt en sträng som matchar och returnerar false för annan indata. Apple dokumenterar destinationens booleska resultat som en indikation på om släppet lyckades. draggable(_:), dropDestination(for:action:isTargeted:)

import SwiftUI

@main
struct DragFeedbackDemoApp: App {
    var body: some Scene {
        WindowGroup {
            DragFeedbackDemo()
        }
    }
}

struct DragFeedbackDemo: View {
    private let payload = "shape.circle"

    @State private var isDragging = false
    @State private var isTargeted = false
    @State private var acceptedCopies = 0

    var body: some View {
        VStack(spacing: 24) {
            Text("Drag the circle into the receiving area")

            Circle()
                .fill(Color.blue)
                .frame(width: 72, height: 72)
                .opacity(isDragging ? 0.4 : 1)
                .draggable(payload)
                .onDragSessionUpdated { session in
                    switch session.phase {
                    case .initial, .active:
                        isDragging = true
                    case .ended, .dataTransferCompleted:
                        restoreFeedback()
                    default:
                        break
                    }
                }

            Text(isDragging ? "Dragging" : "Ready")

            RoundedRectangle(cornerRadius: 12)
                .fill(isTargeted
                      ? Color.blue.opacity(0.2)
                      : Color.gray.opacity(0.12))
                .overlay {
                    Text("Receiving area")
                }
                .frame(width: 280, height: 120)
                .dropDestination(for: String.self) { items, _ in
                    guard items.count == 1,
                          items.first == payload else {
                        return false
                    }
                    acceptedCopies += 1
                    return true
                } isTargeted: { targeted in
                    isTargeted = targeted
                }

            Text("Accepted copies: \(acceptedCopies)")
        }
        .padding(32)
        .frame(minWidth: 380, minHeight: 400)
    }

    private func restoreFeedback() {
        isDragging = false
        isTargeted = false
    }
}

De två flaggorna har olika uppgifter. isDragging ändrar källans opacitet. isTargeted ändrar mottagningsområdets fyllning. När du går in i och lämnar mottagningsområdet kan källan därför inte återställas av misstag medan dragningen fortsätter.

Fasreferensen anger initial som sessionen som snart ska börja, active som rörelse och ended som slutförande. Exemplet återställer återkopplingen vid ended; det gör också samma återställning ofarligt upprepningsbar vid dataTransferCompleted. Det väntar aldrig enbart på att överföringen ska slutföras för att återställa en misslyckad dragning. DragSession.Phase

Apple beskriver dataTransferCompleted som punkten då de dragna objekten har överförts och tillfälliga objekt kan rensas bort. Det här exemplet har inga tillfälliga exportresurser. I en större implementation bör du hålla visuell rensning åtskild från resursrensning, så att återställning av opaciteten inte tar bort något som en överföring fortfarande behöver. DragSession.Phase.dataTransferCompleted

Cirkeln förblir i hierarkin under hela sessionen. Ett godkänt släpp ökar destinationens antal; ett underkänt släpp ändrar inget innehåll. Därför är exemplet användbart för att felsöka återkoppling oberoende av omordning eller flyttsemantik. Koden granskades mot de hämtade deklarationerna; den kompilerades eller kördes inte eftersom den tillgängliga miljön är Linux utan SwiftUI.

Hur bör AppKit-reservlösningen bevara händelseägarskapet?

För en äldre macOS-version som stöds ersätter du implementationen av dragkällan med en NSView som bäddas in via NSViewRepresentable. Representablen skapar och uppdaterar den inbyggda vyn och förmedlar dess återanrop till SwiftUI-tillstånd. SwiftUI styr layouten för den representerade vyn, så använd SwiftUI:s storleksmodifierare i stället för att själv ange den representerade vyns bildruta. NSViewRepresentable

Implementera reservlösningen som en sekvens som ägs av källan:

  1. I den inbyggda källvyns mouseDown(with:) behåller du den faktiska musnedhändelse som levererades till vyn.
  2. I mouseDragged(with:) tillämpar du den valda dragtröskeln och ser till att en andra session inte startas.
  3. Skapa ett NSDraggingItem från en pasteboard-skrivare i minnet som bär den syntetiska objektidentifieraren.
  4. Tillhandahåll en genererad formbild och ange dess dragbildruta i källvyns koordinatsystem.
  5. Anropa beginDraggingSession(with:event:source:) med den sparade musnedhändelsen och objektet som implementerar NSDraggingSource.
  6. Visa källåterkoppling från draggingSession(_:willBeginAt:) och återställ den ovillkorligen från draggingSession(_:endedAt:operation:).
  7. Rensa den sparade inmatningshändelsen och sessionsinformationen när spårningen avslutas. Håll källan och dess återanropsägare vid liv tills sessionen har slutförts.

Dessa detaljer följer AppKits uttryckliga kontrakt: metoden som startar sessionen tar emot den inledande musnedhändelsen, dragobjektets bildrutor använder vyns koordinatsystem och källan får livscykelmeddelanden. NSDraggingItem accepterar ett objekt som följer NSPasteboardWriting; dess bekvämlighetsmetod för en enda bild anger dragbildrutan och innehållet. beginDraggingSession(with:event:source:), init(pasteboardWriter:), setDraggingFrame(_:contents:)

För den här interaktionen i den egna appen returnerar du .copy för NSDraggingContext.withinApplication och en tom åtgärdsuppsättning för andra sammanhang. Det är ett uttryckligt val att endast erbjuda lokala kopior. AppKit frågar källan vilka åtgärder den tillåter genom draggingSession(_:sourceOperationMaskFor:). draggingSession(_:sourceOperationMaskFor:)

Återställ utseendet för varje slutföranderesultat, inte bara för en lyckad kopiering. Villkora eventuella modelländringar med den åtgärd som appen faktiskt implementerade. Slutförandeåteranropets position anges i skärmkoordinater; jämför den inte direkt med en SwiftUI-destinations lokala gränser. draggingSession(_:endedAt:operation:)

En genomskinlig inbyggd vy som placeras bakom en befintlig SwiftUI .draggable blir inte källan för den sessionen bara genom att följa NSDraggingSource. Källan är objektet som skickas med när sessionen startar. Välj en ägare för initiering och slutförande, och begränsa den inbyggda dragytan till formens avsedda interaktionsområde.

Varför inte använda övervakning av musuppsläpp eller avallokering av providern?

En appomfattande övervakare av musuppsläpp observerar inmatningshantering, vilket är en annan gräns än när en dragning slutförs. Apple dokumenterar att lokala händelseövervakare inte tar emot händelser som förbrukas av nästlade spårningsloopar. Den begränsningen gör en övervakare till ett olämpligt substitut för källans dokumenterade återanrop vid slutförande. addLocalMonitorForEvents(matching:handler:)

På samma sätt rapporterar en objekttillhandahållares deinit objektets livstid. Swift anropar en deinitializer omedelbart innan en instans avallokeras; det kontraktet fastställer inte när en dragning avslutades. Om providerns förstöring används som en signal för slutförande kopplas gränssnittets tillstånd till referensägarskap i stället för dragningens livscykel. Deinitialization

Att lägga till en andra DragGesture bredvid en systemdragning har samma begreppsmässiga problem: dess tillstånd tillhör den gesten. Använd @GestureState när den lokala gesten äger interaktionen; använd dragkällans återanrop för sessionen när systemdragningen äger den. GestureState

Verifiera lösningen med en matris över slutföranden

Använd den syntetiska cirkeln för att kontrollera varje väg separat. Det här är förväntade beteenden för implementationen, inte rapporterade testresultat.

Interaktion Förväntad källåterkoppling Förväntat destinationsresultat
Släpp cirkeln i mottagningsområdet Återgår till normalt utseende Antalet ökar en gång
Släpp över tom yta i fönstret Återgår till normalt utseende Antalet förblir oförändrat
Lämna mottagningsområdet medan du fortfarande drar Förblir nedtonad Målmarkeringen rensas
Gå in igen och slutför ett giltigt släpp Återgår till normalt utseende när dragningen avslutas Antalet ökar en gång
Släpp utanför appfönstret utan ett mottagande mål Återgår till normalt utseende Antalet förblir oförändrat
Avbryt en aktiv dragning Återgår till normalt utseende Antalet förblir oförändrat
Upprepa flera misslyckade dragningar Varje session återställs normalt Inga godkända kopior ackumuleras

Om källan förblir nedtonad kontrollerar du först att dess slutförandeobservatör omsluter den faktiska dragkällan och att källan finns kvar. Om bara målmarkeringen fastnar undersöker du målets hantering av in- och utträde samt den gemensamma rensningen. Om det godkända antalet ändras vid ett avvisat släpp undersöker du validering och modelländring separat från visuell återställning.

Den hållbara lösningen är att tydliggöra vem som äger slutförandet: källan återställer återkoppling för hela sessionen, destinationen hanterar sitt eget hovringstillstånd och godkänt innehåll, och överföringsrensning följer sin dokumenterade livscykel. När ansvarsområdena är åtskilda lämnar ett släpp utanför alla mål inget visuellt tillstånd som väntar på ett återanrop från en destination.

Källor