Gedimmte Formen, Umrandungen und Drag-Beschriftungen im Callback zum Sitzungsende der Drag-Quelle wiederherstellen. Der Exit-Callback eines Ziels bedeutet nur, dass der Zeiger dieses Ziel verlassen hat. Verwenden Sie für eine Mac-App die Session-API der SwiftUI-Quelle, sofern verfügbar, oder überlassen Sie einer AppKit-Quelle die Drag-Operation.

Diese Anleitung behandelt eine Aufgabe: Eine synthetische Form soll nach dem Ziehen wieder ihr normales Aussehen erhalten, auch wenn kein Ziel den Drop annimmt. Das Beispiel hält seine Inhalte im Arbeitsspeicher und behandelt einen angenommenen Drop als Kopie. Daher muss beim Wiederherstellen des Feedbacks die Quelle weder gelöscht noch verschoben werden.

Warum bleibt das Drag-Feedback außerhalb von Drop-Zielen aktiv?

Eine gängige Implementierung setzt beim Start des Ziehens ein Flag isDragging und löscht es in performDrop. Damit wird die Bereinigung an ein Ereignis geknüpft, das nur ein annehmendes Ziel verarbeiten kann. Wird die Maustaste über einem leeren Bereich des Fensters losgelassen, ist kein Ziel dafür zuständig, die Quelle zurückzusetzen.

Apple definiert DropDelegate als Schnittstelle für eine View, die so angepasst wurde, dass sie Drops annimmt. Die Methode dropExited(info:) meldet, dass ein validierter Drag diese View verlassen hat. Der Drag kann zu einem anderen Ziel weiterlaufen, zum ersten Ziel zurückkehren oder an anderer Stelle enden. Das Verlassen eines Ziels kann daher nicht belegen, dass die Sitzung der Quelle beendet wurde. DropDelegate

Dieselbe Unterscheidung gilt für dropDestination(for:action:isTargeted:): isTargeted gibt an, ob sich der Zeiger innerhalb des Zielbereichs befindet. Verwenden Sie dies für den Rahmen oder Hintergrund des Ziels. Ein Callback der Quellsitzung sollte das Dimmen der Quelle oder die Drag-Beschriftung steuern. dropDestination(for:action:isTargeted:)

Aus diesen dokumentierten Abgrenzungen ergibt sich eine nützliche Gestaltungsregel: Ordnen Sie jeden visuellen Zustand dem Ereignisverantwortlichen zu, der ihn zuverlässig beenden kann.

Feedback oder Ergebnis Verantwortlich Zeitpunkt für Zurücksetzen oder Übernehmen
Gedimmte Quellform Drag-Quelle Sitzung der Quelle endet
Hervorgehobenes Ziel Drop-Ziel Zeiger verlässt das Ziel; bei einem lokalen Drag zusätzlich vorsorgliche Bereinigung nach Abschluss der Quelle
Angenommenes synthetisches Element Ziel/Modell Drop wird validiert und angenommen
Temporäre exportierte Ressourcen Übertragungslebenszyklus Übertragung abgeschlossen, falls zutreffend

Welchen unterstützten Ansatz sollte eine Mac-App verwenden?

Apples aktuelle Referenz führt onDragSessionUpdated(_:) als ab macOS 26 verfügbar auf. Der Modifier beobachtet Aktualisierungen der nächstgelegenen Drag-Sitzung in der untergeordneten View-Hierarchie. Platzieren Sie ihn direkt außerhalb des Drag-Modifiers der Quelle, damit sein Geltungsbereich klar ist. Verwenden Sie ein SDK, das die Methode tatsächlich deklariert; Laufzeitverfügbarkeit sorgt nicht dafür, dass sich eine fehlende SDK-Deklaration kompilieren lässt. onDragSessionUpdated(_:)

Bei der Dokumentation gibt es eine Unstimmigkeit, die sich gezielt berücksichtigen lässt: Das Beispiel des Modifiers zeigt eine beendete Phase mit Argumenten für Ort und Operation, während die separate Phasenreferenz ended(DropOperation) deklariert. Das folgende Beispiel vergleicht mit .ended, ohne zugehörige Werte auszulesen, da für die visuelle Wiederherstellung keine davon benötigt werden. Prüfen Sie die Deklaration Ihres installierten SDKs, bevor Sie operationsspezifisches Verhalten hinzufügen. DragSession.Phase.ended(_:)

Wenn Ihr Deployment-Target oder SDK den SwiftUI-Callback nicht verwenden kann, bietet eine AppKit-Drag-Quelle den dokumentierten Abschlusszeitpunkt. Apple erklärt ausdrücklich, dass eine Quelle draggingSession(_:endedAt:operation:) erhält, wenn ihr Drag endet oder abgebrochen wird. Eine benutzerdefinierte Quelle muss die Sitzung selbst starten. beginDraggingSession(with:event:source:)

Wenn Sie eine Form nur innerhalb einer einzelnen Canvas neu positionieren müssen, ziehen Sie stattdessen eine lokale DragGesture-Geste einer systemweiten Drag-and-Drop-Sitzung vor. @GestureState wird zurückgesetzt, wenn die Geste inaktiv wird, und eignet sich daher für das vorübergehende Feedback dieser Geste. Behandeln Sie diese Wahl getrennt von einem System-Drag, der übertragbare Inhalte an Ziele übergibt. GestureState

Bereinigung in SwiftUI der Quelle zuordnen

Erstellen Sie eine macOS-SwiftUI-App mit einem SDK, das onDragSessionUpdated(_:) bereitstellt, setzen Sie das Deployment-Target auf macOS 26 oder neuer und ersetzen Sie die generierten Deklarationen für App und Content-View durch dieses vollständige Programm. Es stellt einen synthetischen String shape.circle und einen Empfangsbereich bereit.

Die Quelle verwendet draggable(_:), um übertragbare Inhalte bereitzustellen. Das Ziel nimmt genau einen passenden String an und gibt für andere Eingaben false zurück. Apple dokumentiert, dass der boolesche Rückgabewert des Ziels angibt, ob der Drop erfolgreich war. 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
    }
}

Die beiden Flags haben unterschiedliche Aufgaben. isDragging ändert die Deckkraft der Quelle. isTargeted ändert die Füllfarbe des Empfangsbereichs. Das Betreten und Verlassen des Empfangsbereichs kann daher die Quelle nicht versehentlich wiederherstellen, während der Drag noch läuft.

Die Phasenreferenz beschreibt initial als die Sitzung, die gleich beginnt, active als Bewegung und ended als Abschluss. Das Beispiel stellt das Feedback bei ended wieder her; außerdem wiederholt es denselben Reset bei dataTransferCompleted auf harmlose Weise. Es wartet nie ausschließlich auf den Abschluss der Übertragung, um einen nicht angenommenen Drag wiederherzustellen. DragSession.Phase

Apple beschreibt dataTransferCompleted als den Zeitpunkt, an dem die gezogenen Elemente übertragen wurden und temporäre Elemente bereinigt werden können. Dieses Beispiel verwendet keine temporären Exportressourcen. Trennen Sie in einer größeren Implementierung die visuelle Bereinigung von der Ressourcenbereinigung, damit das Wiederherstellen der Deckkraft nichts verwirft, was eine Übertragung noch benötigt. DragSession.Phase.dataTransferCompleted

Der Kreis bleibt während der gesamten Sitzung in der View-Hierarchie. Ein angenommener Drop erhöht den Zähler des Ziels; ein nicht angenommener Drop ändert keine Inhalte. So eignet sich das Beispiel zur Diagnose des Feedbacks unabhängig von Umordnungs- oder Verschiebungssemantik. Der Code wurde anhand der abgerufenen Deklarationen geprüft, aber nicht kompiliert oder ausgeführt, da die verfügbare Umgebung Linux ohne SwiftUI ist.

Wie sollte der AppKit-Fallback die Ereignisverantwortung wahren?

Ersetzen Sie für eine ältere unterstützte macOS-Version die Drag-Quellenimplementierung durch eine NSView, die über NSViewRepresentable eingebunden wird. Der Representable erstellt und aktualisiert die native View und überbrückt ihre Callbacks mit dem SwiftUI-Zustand. SwiftUI steuert das Layout der repräsentierten View. Verwenden Sie daher SwiftUI-Größenmodifier, statt den Frame der repräsentierten View selbst festzulegen. NSViewRepresentable

Implementieren Sie den Fallback als eine Sequenz, für die die Quelle verantwortlich ist:

  1. Speichern Sie in mouseDown(with:) der nativen Quell-View das tatsächliche Mousedown-Ereignis, das an diese View übergeben wurde.
  2. Wenden Sie in mouseDragged(with:) den von Ihnen gewählten Schwellenwert für den Drag an und verhindern Sie den Start einer zweiten Sitzung.
  3. Erstellen Sie ein NSDraggingItem aus einem In-Memory-Pasteboard-Writer, der den Bezeichner des synthetischen Elements enthält.
  4. Stellen Sie ein generiertes Formbild bereit und legen Sie dessen Drag-Frame im Koordinatensystem der Quell-View fest.
  5. Rufen Sie beginDraggingSession(with:event:source:) mit dem gespeicherten Mousedown-Ereignis und dem Objekt auf, das NSDraggingSource implementiert.
  6. Zeigen Sie das Feedback der Quelle in draggingSession(_:willBeginAt:) an und stellen Sie es in draggingSession(_:endedAt:operation:) unbedingt wieder her.
  7. Löschen Sie das gespeicherte Eingabeereignis und die Sitzungsdaten, wenn das Tracking endet. Halten Sie Quelle und Callback-Eigentümer bis zum Abschluss der Sitzung am Leben.

Diese Details folgen dem ausdrücklichen Vertrag von AppKit: Die Methode zum Starten der Sitzung erhält das auslösende Mousedown-Ereignis, Frames für Drag-Elemente verwenden das Koordinatensystem der View, und die Quelle erhält Lifecycle-Nachrichten. NSDraggingItem akzeptiert ein Objekt, das NSPasteboardWriting entspricht; die Hilfsmethode für ein einzelnes Bild legt den Drag-Frame und den Inhalt fest. beginDraggingSession(with:event:source:), init(pasteboardWriter:), setDraggingFrame(_:contents:)

Geben Sie für diese Interaktion innerhalb der eigenen App .copy für NSDraggingContext.withinApplication und für andere Kontexte eine leere Operationsmenge zurück. Damit werden ausdrücklich nur lokale Kopien angeboten. AppKit fragt die Quelle über draggingSession(_:sourceOperationMaskFor:) ab, welche Operationen sie zulässt. draggingSession(_:sourceOperationMaskFor:)

Stellen Sie das Aussehen bei jedem Abschlussergebnis wieder her, nicht nur bei einer erfolgreichen Kopie. Nehmen Sie Modelländerungen nur vor, wenn Ihre App die entsprechende Operation tatsächlich implementiert hat. Der Punkt im Abschluss-Callback verwendet Bildschirmkoordinaten; vergleichen Sie ihn nicht direkt mit den lokalen Bounds eines SwiftUI-Ziels. draggingSession(_:endedAt:operation:)

Eine transparente native View hinter einem vorhandenen SwiftUI-.draggable wird nicht allein dadurch zur Quelle dieser Sitzung, dass sie NSDraggingSource entspricht. Quelle ist das Objekt, das beim Starten der Sitzung übergeben wurde. Bestimmen Sie einen einzigen Verantwortlichen für Start und Abschluss und beschränken Sie die native Drag-Fläche auf den vorgesehenen Interaktionsbereich der Form.

Warum keine Überwachung des Loslassens oder Deinitialisierung des Providers verwenden?

Ein anwendungsweiter Monitor für Mouse-up-Ereignisse beobachtet die Eingabeverarbeitung. Das ist ein anderer Zeitpunkt als der Abschluss eines Drags. Apple dokumentiert, dass lokale Ereignismonitore keine Ereignisse erhalten, die von verschachtelten Tracking-Schleifen verarbeitet werden. Daher eignet sich ein Monitor nicht als Ersatz für den dokumentierten Abschluss-Callback der Quelle. addLocalMonitorForEvents(matching:handler:)

Ebenso meldet deinit eines Item-Providers die Lebensdauer des Objekts. Swift ruft einen Deinitializer unmittelbar vor der Deallokation einer Instanz auf; dieser Vertrag legt nicht fest, wann ein Drag beendet wurde. Die Zerstörung des Providers als Abschlusssignal zu verwenden, koppelt den UI-Zustand an die Referenzverwaltung statt an den Drag-Lifecycle. Deinitialization

Ein zusätzliches DragGesture neben einem System-Drag zu verwenden, hat dasselbe konzeptionelle Problem: Sein Zustand gehört zu dieser Geste. Verwenden Sie @GestureState, wenn die lokale Geste die Interaktion steuert, und den Callback der Quellsitzung, wenn der System-Drag dafür verantwortlich ist. GestureState

Den Fix mit einer Abschlussmatrix überprüfen

Verwenden Sie den synthetischen Kreis, um jeden Ablauf einzeln zu prüfen. Dabei handelt es sich um erwartetes Verhalten der Implementierung, nicht um berichtete Testergebnisse.

Interaktion Erwartetes Feedback der Quelle Erwartetes Ergebnis des Ziels
Den Kreis innerhalb des Empfangsbereichs ablegen Kehrt zum normalen Aussehen zurück Zähler wird einmal erhöht
Über einem leeren Bereich des Fensters loslassen Kehrt zum normalen Aussehen zurück Zähler bleibt unverändert
Den Empfangsbereich verlassen, während der Drag noch läuft Bleibt gedimmt Zielhervorhebung wird aufgehoben
Erneut hineingehen und einen gültigen Drop abschließen Kehrt beim Abschluss zum normalen Aussehen zurück Zähler wird einmal erhöht
Außerhalb des App-Fensters loslassen, ohne dass ein Ziel den Drop annimmt Kehrt zum normalen Aussehen zurück Zähler bleibt unverändert
Einen aktiven Drag abbrechen Kehrt zum normalen Aussehen zurück Zähler bleibt unverändert
Mehrere erfolglose Drags wiederholen Jede Sitzung wird normal zurückgesetzt Keine zusätzlichen angenommenen Kopien

Bleibt die Quelle gedimmt, prüfen Sie zunächst, ob der Abschlussbeobachter die tatsächliche Drag-Quelle umschließt und ob die Quelle weiterhin vorhanden ist. Bleibt nur die Zielhervorhebung bestehen, überprüfen Sie die Ein- und Austrittsbehandlung des Ziels sowie die gemeinsame Bereinigung. Ändert sich der Zähler bei einem abgelehnten Drop, untersuchen Sie die Validierung und die Modelländerung getrennt von der visuellen Wiederherstellung.

Die dauerhafte Lösung besteht darin, die Verantwortung für den Abschluss eindeutig zuzuordnen: Die Quelle stellt das sitzungsweite Feedback wieder her, das Ziel behandelt seinen eigenen Hover-Zustand und angenommene Inhalte, und die Bereinigung der Übertragung folgt ihrem dokumentierten Lifecycle. Wenn diese Aufgaben getrennt sind, bleibt beim Ablegen außerhalb aller Ziele kein visueller Zustand zurück, der auf einen Callback des Ziels wartet.

Quellen