Per il drag and drop all’interno della tua app per Mac, la sorgente dichiara le operazioni supportate, la destinazione ne sceglie una e il modello applica il risultato. In SwiftUI su macOS 26 o versioni successive, configura entrambi gli estremi e rimuovi l’originale solo dopo uno spostamento completato.
Questo articolo realizza una semplice interazione desktop: trascinare un cerchio sintetico in Copia qui oppure in Sposta qui. Tutto resta in memoria. L’esempio rende espliciti l’operazione, l’identità dell’elemento e la pulizia della sorgente, quindi associa le stesse responsabilità ad AppKit.
Su cosa devono concordare la sorgente e la destinazione?
Un payload trasferibile descrive cosa viene trasferito tra le viste. Copia o spostamento descrive cosa accade all’originale. Sono decisioni separate. La documentazione SwiftUI di Apple afferma che il trascinamento supporta la copia per impostazione predefinita; abilitando lo spostamento si aggiunge un’altra operazione supportata. La destinazione sceglie quindi come ricevere l’elemento. DragConfiguration
Per progettare l’interazione, considera tre responsabilità:
| Responsabilità | Copia | Spostamento |
|---|---|---|
| Capacità della sorgente | Offre la copia | Offre lo spostamento |
| Conferma della destinazione | Inserisce un elemento indipendente | Riceve l’elemento esistente |
| Completamento della sorgente | Mantiene l’originale | Rimuove l’originale dopo lo spostamento completato |
In questo esempio del cerchio, una copia indipendente riceve un nuovo identificatore. Uno spostamento mantiene l’identificatore cambiando la posizione dell’elemento. È una scelta di progettazione dell’applicazione, coerente con il requisito di Swift secondo cui un’entità identificabile deve avere un’identità stabile; Swift lascia all’applicazione la scelta dell’ambito e della durata di tale identità. Identifiable
La conseguenza pratica è che restituire un’operazione di spostamento non trasferisce un elemento tra i tuoi array. Devi comunque inserire l’elemento nella destinazione e rimuoverlo dalla sorgente. Apple mostra esplicitamente come osservare il completamento dell’operazione di trascinamento per indicare al modello quando rimuovere un elemento dalla sorgente. onDragSessionUpdated(_:)
Quali API SwiftUI dovrebbe usare un’app per Mac?
Per questa implementazione, usa un deployment target macOS 26 o successivo e un SDK che includa i modificatori di configurazione documentati. Aggiungi draggable(_:) alla sorgente, dragConfiguration(_:) per dichiararne le operazioni e dropConfiguration(_:) per selezionare l’operazione della destinazione. La disponibilità di entrambi i modificatori di configurazione è documentata a partire da macOS 26. dragConfiguration(_:), dropConfiguration(_:)
L’overload attuale di dropDestination(for:isEnabled:action:) fornisce un DropSession alla propria action. L’action restituisce Void. È diverso dal vecchio overload, la cui action riceve una posizione e restituisce un valore booleano. Nell’esempio specifica esplicitamente isEnabled: per rendere chiaro quale overload viene usato. dropDestination(for:isEnabled:action:)
Nella destinazione, esamina suggestedOperations prima di selezionare un’operazione. Per una destinazione generica che privilegia lo spostamento, scegli lo spostamento se è disponibile, altrimenti la copia se è disponibile e, in caso contrario, l’operazione vietata. L’esempio documentato da Apple usa questo ripiego dallo spostamento alla copia e specifica che la closure di configurazione viene eseguita spesso, quindi è bene mantenerla poco costosa. dropConfiguration(_:)
Le nostre due destinazioni hanno deliberatamente significati fissi. Copia qui accetta la copia; Sposta qui accetta lo spostamento. Una destinazione restituisce l’operazione vietata quando quella indicata non è disponibile. In questo modo il risultato è prevedibile senza richiedere un modificatore da tastiera.
Crea un esempio completo di copia e spostamento in memoria
Crea un’app SwiftUI per macOS, imposta il deployment target su macOS 26 o successivo e sostituisci le dichiarazioni generate dell’app e della vista dei contenuti con il seguente file singolo. L’esempio trasferisce una stringa integrata che contiene l’identificatore di un elemento, quindi non servono né un tipo trasferibile personalizzato né una dichiarazione di tipo personalizzata. Apple documenta sia i payload trasferibili per le sorgenti di trascinamento sia i payload stringa per le destinazioni di rilascio. draggable(_:), dropDestination(for:isEnabled:action:)
Questa è un’app minima completa, pensata per una sola finestra aperta e un solo elemento sorgente. Ho confrontato i riferimenti alle API con la documentazione consultata; qui non ho potuto compilarla né eseguirla perché Swift e l’SDK macOS non sono disponibili.
import SwiftUI
import Foundation
@main
struct CopyMoveDemoApp: App {
var body: some Scene {
WindowGroup {
CopyMoveDemo()
}
}
}
struct CopyMoveDemo: View {
@State private var sourceID = UUID()
@State private var sourcePresent = true
@State private var copiedIDs: [UUID] = []
@State private var movedIDs: [UUID] = []
@State private var moveReceived = false
private var sourceConfiguration: DragConfiguration {
DragConfiguration(
operationsWithinApp: .init(allowMove: true),
operationsOutsideApp: .init(
allowCopy: false,
allowMove: false,
allowDelete: false
)
)
}
var body: some View {
VStack(spacing: 24) {
Text("Drag the circle to a target")
.font(.headline)
if sourcePresent {
Circle()
.fill(.blue)
.frame(width: 90, height: 90)
.overlay(Text("Circle").foregroundStyle(.white))
.draggable(sourceID.uuidString)
.dragConfiguration(sourceConfiguration)
.onDragSessionUpdated { session in
if session.phase == .initial {
moveReceived = false
}
if session.phase == .ended(.move), moveReceived {
sourcePresent = false
}
if case .ended = session.phase {
moveReceived = false
}
}
} else {
Text("Original moved")
.frame(width: 90, height: 90)
}
HStack(alignment: .top, spacing: 20) {
target(moving: false)
target(moving: true)
}
}
.padding(24)
.frame(minWidth: 520, minHeight: 320)
}
private func target(moving: Bool) -> some View {
let operation: DropOperation = moving ? .move : .copy
let ids = moving ? movedIDs : copiedIDs
return VStack(spacing: 10) {
Text(moving ? "Move here" : "Copy here")
.font(.headline)
ForEach(ids, id: \.self) { id in
Text("Circle \(id.uuidString.prefix(4))")
}
Spacer(minLength: 20)
}
.frame(width: 210, height: 150)
.background(.blue.opacity(0.08))
.contentShape(Rectangle())
.dropDestination(for: String.self, isEnabled: true) {
tokens, session in
guard session.localSession != nil,
sourcePresent,
tokens == [sourceID.uuidString] else {
return
}
if moving {
guard !movedIDs.contains(sourceID) else { return }
movedIDs.append(sourceID)
moveReceived = true
} else {
copiedIDs.append(UUID())
}
}
.dropConfiguration { session in
guard sourcePresent,
session.localSession != nil,
session.itemsCount == 1,
session.suggestedOperations.contains(operation) else {
return DropConfiguration(operation: .forbidden)
}
return DropConfiguration(operation: operation)
}
}
}
La configurazione della sorgente offre copia e spostamento all’interno dell’app. Disabilita le operazioni all’esterno dell’app usando l’inizializzatore macOS per le operazioni esterne. Anche la destinazione controlla localSession, che, secondo la documentazione Apple, è assente quando il trascinamento proviene da un’altra app. Queste impostazioni mantengono l’esempio concentrato sull’elemento in memoria della propria finestra. DragConfiguration.OperationsWithinApp, init(allowCopy:allowMove:allowDelete:), DropSession
L’action di rilascio convalida il token esatto prima di inserire qualsiasi elemento. La copia aggiunge un nuovo identificatore. Lo spostamento aggiunge l’identificatore originale e registra che questa destinazione l’ha ricevuto. La sorgente rimuove quindi l’originale visualizzato solo quando la sessione termina con uno spostamento e tale ricezione è stata registrata. Per questa dimostrazione limitata, la registrazione è una salvaguardia a livello di applicazione, non una funzionalità di transazione di SwiftUI.
L’attuale dichiarazione delle fasi di trascinamento di Apple include initial, active, ending, ended e dataTransferCompleted. L’esempio usa il caso documentato ended(DropOperation) per il risultato dell’operazione. Non rimuove la sorgente durante il passaggio del puntatore né nella fase precedente di conclusione. DragSession.Phase
Mantieni espliciti la proprietà e i confini del gesto
La stringa è un token di ricerca, non il modello completo del cerchio. La destinazione lo cerca nello stato della sorgente gestito da questa vista. Per più documenti o finestre, sostituisci questa ricerca locale con un proprietario esplicito del modello e una registrazione associata alla sessione di trascinamento e agli elementi effettivi. Il fatto che un trascinamento abbia origine all’interno dell’app non determina, da solo, a quale documento appartenga un elemento.
Per più elementi trascinabili, usa identificatori stabili del modello e, quando disponibile, ricava dalla sessione gli identificatori trascinati. Apple mette a disposizione draggedItemIDs(for:) per payload identificabili o per identificatori forniti separatamente. Evita di rimuovere gli elementi selezionati in quel momento al completamento: la selezione e l’identità degli elementi trascinati sono dati distinti. DragSession
Lascia che il modificatore draggable(_:) di SwiftUI installi l’interazione di trascinamento. Evita di rimuovere elementi dal modello durante la creazione del payload. Apple documenta che il modificatore aggiunge i gesti appropriati e sconsiglia di inoltrare a un attore principale una closure di esportazione. draggable(_:)
Come funziona lo stesso accordo in AppKit?
Usa AppKit quando il tuo deployment target o l’architettura delle viste esistente richiede il suo ciclo di vita esplicito del trascinamento. Il suo protocollo della sorgente dichiara le operazioni consentite tramite draggingSession(_:sourceOperationMaskFor:). Il contesto permette di distinguere le destinazioni interne all’app da quelle esterne. Per l’interazione descritta sopra, offri copia e spostamento all’interno dell’app e nessuna operazione all’esterno. draggingSession(_:sourceOperationMaskFor:)
La destinazione si registra per il tipo di pasteboard pertinente, quindi sceglie un’operazione in draggingEntered(_:) e, se necessario, aggiorna tale scelta in draggingUpdated(_:). Apple richiede che la destinazione consideri la sovrapposizione tra le operazioni della sorgente e le proprie operazioni e i tipi di dati supportati. Restituisci una sola operazione oppure nessuna se non c’è una sovrapposizione accettabile. NSDraggingDestination, draggingEntered(_:)
Esegui l’inserimento in performDragOperation(_:), dopo aver completato correttamente la preparazione. Restituisci true solo se la destinazione accetta i dati. La sorgente riceve l’operazione completata tramite draggingSession(_:endedAt:operation:); usa quel risultato, insieme alle regole di proprietà del modello, per completare la rimozione dal lato della sorgente in caso di spostamento. performDragOperation(_:), draggingSession(_:endedAt:operation:)
Se incorpori una superficie di trascinamento AppKit in un’interfaccia SwiftUI, lascia che sia quella superficie a gestire l’interazione AppKit. beginDraggingSession(with:event:source:) riceve l’evento iniziale di pressione del mouse e i riquadri degli elementi trascinati usano il sistema di coordinate della vista. Mantieni la relazione tra l’evento e le coordinate invece di creare un evento artificiale a partire da un valore successivo del gesto SwiftUI. beginDraggingSession(with:event:source:)
Il codice precedente della destinazione SwiftUI può restituire DropProposal(operation: .move) da DropDelegate.dropUpdated(info:). Questo esprime la proposta della destinazione. Non sostituisce la configurazione delle capacità della sorgente, l’osservazione dell’operazione completata o le modifiche al modello. Quando le API più recenti per la sorgente SwiftUI non sono disponibili e serve il controllo esplicito di entrambi gli estremi, il ciclo di vita della sorgente e della destinazione AppKit offre questa possibilità. dropUpdated(info:), NSDraggingSource
Verifica insieme l’operazione e il modello
Per l’esempio, usa questa lista degli stati attesi. Sono condizioni di accettazione derivate dall’implementazione, non risultati di test riportati:
| Azione | Originale | Destinazione di copia | Destinazione di spostamento |
|---|---|---|---|
Rilascio una volta su Copia qui |
Rimane | Aggiunge un nuovo identificatore | Invariata |
| Copia di nuovo | Rimane | Aggiunge un altro nuovo identificatore | Invariata |
Rilascio su Sposta qui |
Scompare dopo lo spostamento completato | Invariata | Riceve l’identificatore originale |
| Annullamento o rilascio lontano da entrambe le destinazioni | Rimane | Invariata | Invariata |
Quando il comportamento non corrisponde alle attese, esamina le fasi in ordine: capacità della sorgente, operazione della destinazione, inserimento nella destinazione e completamento della sorgente. Un originale duplicato dopo lo spostamento indica probabilmente una pulizia mancante della sorgente. Due rimozioni indicano probabilmente due proprietari del modello in conflitto. Una copia inattesa indica probabilmente che la sorgente non ha offerto lo spostamento oppure che la destinazione ha selezionato la copia.
Mantieni chiara la responsabilità finale: l’operazione negoziata descrive il trasferimento, mentre il modello applica la posizione e l’identità dell’elemento. Offri le operazioni supportate, scegli un’operazione consentita nella destinazione e apporta ogni modifica al modello una sola volta, nel punto previsto del ciclo di vita.
Fonti
DragConfiguration — Apple
Identifiable — Apple
onDragSessionUpdated(_:) — Apple
dragConfiguration(_:) — Apple
dropConfiguration(_:) — Apple
draggable(_:) — Apple
DropSession — Apple
DragSession.Phase — Apple
DragSession — Apple
NSDraggingDestination — Apple
draggingEntered(_:) — Apple
performDragOperation(_:) — Apple
dropUpdated(info:) — Apple
NSDraggingSource — Apple