Ripristina le forme attenuate, i contorni e le etichette di trascinamento dal callback di fine sessione della sorgente del trascinamento. Il callback di uscita di una destinazione indica soltanto che il puntatore ha lasciato quell’area. Per un’app Mac, usa l’API SwiftUI della sessione sorgente quando disponibile oppure affida il trascinamento a una sorgente AppKit.
Questa guida tratta un solo compito: riportare una forma sintetica al suo aspetto normale quando l’utente termina il trascinamento, anche se nessuna destinazione la accetta. L’esempio mantiene i contenuti in memoria e considera una copia il rilascio accettato, quindi il ripristino del feedback non richiede mai di eliminare o spostare la sorgente.
Perché il feedback di trascinamento rimane attivo fuori dalle aree di rilascio?
Un’implementazione comune imposta un flag isDragging all’inizio del trascinamento e lo azzera in performDrop. In questo modo la pulizia dipende da un evento che può gestire solo una destinazione che accetta il rilascio. Se si rilascia in uno spazio vuoto della finestra, nessuna destinazione è responsabile del ripristino della sorgente.
Apple definisce DropDelegate come un’interfaccia per una vista modificata in modo da accettare rilasci. Il suo metodo dropExited(info:) segnala che un trascinamento convalidato è uscito da quella vista. Il trascinamento può proseguire verso un’altra destinazione, tornare alla prima oppure terminare altrove. Di conseguenza, l’uscita da una destinazione non dimostra che la sessione della sorgente sia terminata. DropDelegate
La stessa distinzione vale per dropDestination(for:action:isTargeted:): isTargeted indica se il puntatore si trova all’interno dell’area di destinazione. Usalo per il bordo o lo sfondo della destinazione. Affida il controllo dell’attenuazione della sorgente o dell’etichetta di trascinamento a un callback della sessione sorgente. dropDestination(for:action:isTargeted:)
Da questi confini documentati deriva una regola di progettazione utile: assegna ogni stato visivo all’evento proprietario che può terminarlo in modo affidabile.
| Feedback o risultato | Responsabile | Punto di ripristino o conferma |
|---|---|---|
| Forma sorgente attenuata | Sorgente del trascinamento | Fine della sessione sorgente |
| Destinazione evidenziata | Destinazione del rilascio | Uscita del puntatore, con pulizia di sicurezza al completamento della sorgente per un trascinamento locale |
| Elemento sintetico accettato | Destinazione/modello | Il rilascio viene convalidato e riesce |
| Risorse temporanee esportate | Ciclo di vita del trasferimento | Completamento del trasferimento, se applicabile |
Quale approccio supportato dovrebbe usare un’app Mac?
La documentazione di riferimento attuale di Apple indica che onDragSessionUpdated(_:) è disponibile da macOS 26. Osserva gli aggiornamenti della sessione di trascinamento più vicina nella gerarchia delle viste figlie. Applicalo direttamente all’esterno del modificatore di trascinamento della sorgente, così che il suo ambito sia chiaro. Usa un SDK che dichiari effettivamente il metodo: la disponibilità a runtime non permette di compilare se manca la dichiarazione nell’SDK. onDragSessionUpdated(_:)
C’è una discrepanza nella documentazione che conviene gestire in modo circoscritto: l’esempio del modificatore mostra una fase terminata con argomenti relativi alla posizione e all’operazione, mentre il riferimento specifico della fase dichiara ended(DropOperation). L’esempio seguente confronta la fase con .ended senza estrarne i valori associati, perché il ripristino visivo non ne ha bisogno. Consulta la dichiarazione dell’SDK installato prima di aggiungere comportamenti specifici per l’operazione. DragSession.Phase.ended(_:)
Se la versione di destinazione o l’SDK non consentono di usare il callback SwiftUI, una sorgente di trascinamento AppKit offre il confine di completamento documentato. Apple afferma esplicitamente che una sorgente riceve draggingSession(_:endedAt:operation:) quando il trascinamento termina o viene annullato. Una sorgente personalizzata deve avviare autonomamente la sessione. beginDraggingSession(with:event:source:)
Se devi solo riposizionare una forma all’interno di una singola area di disegno, valuta l’uso di un DragGesture locale al posto di una sessione di trascinamento del sistema. @GestureState si azzera quando il gesto diventa inattivo, quindi è adatto al feedback temporaneo di quel gesto. Mantieni questa scelta distinta da un trascinamento di sistema che fornisce contenuti trasferibili alle destinazioni. GestureState
Implementare in SwiftUI la pulizia gestita dalla sorgente
Crea un’app SwiftUI per macOS usando un SDK che esponga onDragSessionUpdated(_:), imposta come versione di destinazione macOS 26 o successiva e sostituisci le dichiarazioni generate per l’app e la vista dei contenuti con questo programma completo. Fornisce una stringa sintetica, shape.circle, e un’area di ricezione.
La sorgente usa draggable(_:) per fornire contenuti trasferibili. La destinazione accetta esattamente una stringa corrispondente e restituisce false per qualsiasi altro input. Apple documenta che il risultato booleano della destinazione indica se il rilascio è riuscito. 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
}
}
I due flag hanno funzioni diverse. isDragging modifica l’opacità della sorgente. isTargeted modifica il riempimento dell’area di ricezione. Entrare e uscire dall’area di ricezione non può quindi ripristinare accidentalmente la sorgente mentre il trascinamento prosegue.
Il riferimento della fase identifica initial come la sessione in procinto di iniziare, active come lo spostamento e ended come il completamento. L’esempio ripristina il feedback in ended; ripete inoltre lo stesso ripristino in modo innocuo in dataTransferCompleted. Non aspetta mai esclusivamente il completamento del trasferimento per ripristinare lo stato di un trascinamento non riuscito. DragSession.Phase
Apple descrive dataTransferCompleted come il momento in cui gli elementi trascinati sono stati trasferiti e si possono eliminare gli elementi temporanei. Questo esempio non usa risorse temporanee esportate. In un’implementazione più ampia, tieni separata la pulizia visiva da quella delle risorse, così che il ripristino dell’opacità non elimini qualcosa di cui il trasferimento ha ancora bisogno. DragSession.Phase.dataTransferCompleted
Il cerchio rimane nella gerarchia per tutta la sessione. Un rilascio accettato incrementa il conteggio della destinazione; un rilascio non accettato non modifica alcun contenuto. Questo rende l’esempio utile per diagnosticare il feedback indipendentemente dal riordino o dalla semantica dello spostamento. Il codice è stato confrontato con le dichiarazioni recuperate, ma non è stato compilato né eseguito perché l’ambiente disponibile è Linux e non include SwiftUI.
Come dovrebbe preservare la titolarità degli eventi la soluzione alternativa AppKit?
Per una versione di macOS meno recente supportata, sostituisci l’implementazione della sorgente del trascinamento con una NSView incorporata tramite NSViewRepresentable. Il rappresentable crea e aggiorna la vista nativa e collega i suoi callback allo stato SwiftUI. SwiftUI controlla il layout della vista rappresentata, quindi usa i modificatori di dimensionamento SwiftUI anziché assegnare direttamente il frame della vista rappresentata. NSViewRepresentable
Implementa la soluzione alternativa come un’unica sequenza gestita dalla sorgente:
- In
mouseDown(with:)della vista nativa sorgente, conserva l’evento effettivo di pressione del mouse ricevuto dalla vista. - In
mouseDragged(with:), applica la soglia di trascinamento scelta e impedisci l’avvio di una seconda sessione. - Crea un
NSDraggingItemda un writer per la pasteboard in memoria che contenga l’identificatore dell’elemento sintetico. - Fornisci un’immagine generata della forma e imposta il frame di trascinamento nel sistema di coordinate della vista sorgente.
- Chiama
beginDraggingSession(with:event:source:)usando l’evento di pressione del mouse conservato e l’oggetto che implementaNSDraggingSource. - Mostra il feedback della sorgente da
draggingSession(_:willBeginAt:)e ripristinalo incondizionatamente dadraggingSession(_:endedAt:operation:). - Cancella l’evento di input conservato e i dati della sessione al termine del tracciamento. Mantieni in vita la sorgente e il proprietario del suo callback fino al completamento della sessione.
Questi dettagli seguono il contratto esplicito di AppKit: il metodo di avvio della sessione accetta l’evento iniziale di pressione del mouse, i frame degli elementi trascinati usano il sistema di coordinate della vista e la sorgente riceve i messaggi del ciclo di vita. NSDraggingItem accetta un oggetto conforme a NSPasteboardWriting; il suo metodo di convenienza per una singola immagine imposta il frame e il contenuto di trascinamento. beginDraggingSession(with:event:source:), init(pasteboardWriter:), setDraggingFrame(_:contents:)
Per questa interazione all’interno della propria app, restituisci .copy per NSDraggingContext.withinApplication e un insieme di operazioni vuoto per gli altri contesti. È una scelta esplicita che offre solo copie locali. AppKit chiede alla sorgente quali operazioni consente tramite draggingSession(_:sourceOperationMaskFor:). draggingSession(_:sourceOperationMaskFor:)
Ripristina l’aspetto per ogni risultato del completamento, non solo per una copia riuscita. Limita le modifiche al modello all’operazione effettivamente implementata dall’app. Il punto del callback di completamento è espresso in coordinate dello schermo: non confrontarlo direttamente con i limiti locali di una destinazione SwiftUI. draggingSession(_:endedAt:operation:)
Una vista nativa trasparente collocata dietro un .draggable SwiftUI esistente non diventa la sorgente di quella sessione solo perché è conforme a NSDraggingSource. La sorgente è l’oggetto passato all’avvio della sessione. Scegli un unico responsabile per l’avvio e il completamento e limita la superficie di trascinamento nativa all’area d’interazione prevista per la forma.
Perché non usare il monitoraggio del rilascio del mouse o la deinizializzazione del provider?
Un monitor globale del rilascio del mouse osserva l’inoltro degli input, un confine diverso dal completamento del trascinamento. Apple documenta che i monitor locali degli eventi non ricevono gli eventi consumati da cicli di tracciamento annidati. Questa limitazione rende il monitor un sostituto inadatto del callback di completamento documentato della sorgente. addLocalMonitorForEvents(matching:handler:)
Allo stesso modo, deinit di un provider di elementi segnala la durata dell’oggetto. Swift chiama un deinizializzatore subito prima che un’istanza venga deallocata; questo contratto non stabilisce quando sia terminato un trascinamento. Usare la distruzione del provider come segnale di completamento lega lo stato dell’interfaccia alla proprietà dei riferimenti invece che al ciclo di vita del trascinamento. Deinitialization
Aggiungere un secondo DragGesture accanto a un trascinamento di sistema presenta lo stesso problema concettuale: il suo stato appartiene a quel gesto. Usa @GestureState quando l’interazione è gestita dal gesto locale; usa il callback della sessione della sorgente quando è gestita dal trascinamento di sistema. GestureState
Verificare la correzione con una matrice di completamento
Usa il cerchio sintetico per controllare separatamente ogni percorso. Sono comportamenti attesi per l’implementazione, non risultati di test riportati.
| Interazione | Feedback atteso della sorgente | Risultato atteso della destinazione |
|---|---|---|
| Rilascia il cerchio all’interno dell’area di ricezione | Torna normale | Il conteggio aumenta di uno |
| Rilascia in uno spazio vuoto della finestra | Torna normale | Il conteggio resta invariato |
| Esci dall’area di ricezione mentre il trascinamento è ancora in corso | Rimane attenuato | L’evidenziazione della destinazione si disattiva |
| Rientra e completa un rilascio valido | Torna normale al completamento | Il conteggio aumenta di uno |
| Rilascia fuori dalla finestra dell’app senza una destinazione che accetti il rilascio | Torna normale | Il conteggio resta invariato |
| Annulla un trascinamento attivo | Torna normale | Il conteggio resta invariato |
| Ripeti più trascinamenti non riusciti | Ogni sessione ripristina normalmente lo stato | Nessun accumulo di copie accettate |
Se la sorgente rimane attenuata, controlla innanzitutto che l’osservatore del completamento avvolga la sorgente di trascinamento effettiva e che la sorgente sia ancora presente. Se si blocca solo l’evidenziazione della destinazione, esamina la gestione dell’entrata e dell’uscita e la pulizia condivisa. Se il conteggio degli elementi accettati cambia dopo un rilascio rifiutato, verifica la convalida e la modifica del modello separatamente dal ripristino visivo.
La correzione duratura consiste nel rendere esplicita la titolarità del completamento: la sorgente ripristina il feedback dell’intera sessione, la destinazione gestisce il proprio stato di passaggio e i contenuti accettati, mentre la pulizia del trasferimento segue il ciclo di vita documentato. Separando queste responsabilità, il rilascio fuori da tutte le aree di destinazione non lascia stati visivi in attesa del callback di una destinazione.
Fonti
onDragSessionUpdated(_:) — Apple
DragSession.Phase — Apple
DragSession.Phase.ended(_:) — Apple
draggable(_:) — Apple
DropDelegate — Apple
GestureState — Apple
NSViewRepresentable — Apple
init(pasteboardWriter:) — Apple
setDraggingFrame(_:contents:) — Apple
Deinitialization — Swift.org