Per far apparire integra una scheda sintetica durante un trascinamento nativo di macOS, assegna al contenitore esterno un’unica origine di trascinamento e usa l’intera scheda come anteprima. Usa AppKit quando hai bisogno di controllare esplicitamente il riquadro iniziale dell’anteprima e la posizione del puntatore.

Questa guida spiega come trascinare una scheda in memoria tra due pannelli della tua app per Mac. L’obiettivo pratico è preservare le forme, il testo, la spaziatura e l’identità della scheda, mantenendo al contempo una gestione chiara dei gesti e degli aggiornamenti del modello.

Quale parte del trascinamento deve cambiare?

Apple separa i dati trasferiti dalla rappresentazione visiva mostrata durante il trascinamento. draggable(_:preview:) di SwiftUI genera il rendering della vista di anteprima quando inizia il trascinamento. La documentazione specifica che l’anteprima è centrata sulla vista sorgente. La vista sorgente stessa non viene trasferita. Consulta la documentazione di draggable(_:preview:) di Apple.

Usa questo schema decisionale per individuare il controllo di cui hai davvero bisogno:

Comportamento desiderato Punto di partenza supportato
Mantenere insieme l’etichetta, le forme e lo sfondo della scheda Un’unica origine di trascinamento esterna con un’unica anteprima personalizzata completa
Regolare il contorno durante la transizione di sollevamento contentShape(.dragPreview, shape)
Modificare quale parte della scheda può avviare il trascinamento contentShape(.interaction, shape)
Aggiungere una scheda a un altro pannello Convalidare il payload e aggiornare il modello nella destinazione
Impostare esplicitamente il riquadro iniziale dell’immagine di trascinamento e la posizione del puntatore Una sessione gestita da AppKit con NSDraggingItem
Spostare continuamente un elemento all’interno di un canvas senza destinazioni di rilascio native Un’interazione locale con DragGesture

La distinzione tra i due tipi di forma del contenuto è importante: Apple documenta che .dragPreview influisce sulla forma dell’anteprima, mentre .interaction controlla il rilevamento dei tocchi. Modificare solo il contorno dell’anteprima non estende la regione che avvia il trascinamento. Consulta ContentShapeKinds.dragPreview.

Un DragGesture locale segnala un movimento di trascinamento tramite una sequenza di gesti. Scegli questo approccio quando devi aggiornare la posizione nel canvas. Per l’attività di rilascio nativo tra pannelli descritta in questo articolo, lascia che draggable installi i gesti di trascinamento. Consulta il riferimento di Apple a DragGesture e la guida sulle origini di trascinamento.

Crea un’unica anteprima per l’intera scheda sintetica

Applica il modificatore di trascinamento al contenitore più piccolo che include tutto ciò che deve spostarsi insieme. Per una scheda composta da un cerchio, due righe di testo e uno sfondo arrotondato, il contenitore è l’intera scheda. Applicare origini di trascinamento separate ai suoi elementi secondari descriverebbe interazioni distinte.

Riutilizza la stessa vista di presentazione sia per l’origine sia per l’anteprima. Assegna a tale vista dimensioni esplicite e uno sfondo proprio, invece di dipendere dallo stile ereditato da un pannello circostante. È una raccomandazione progettuale dedotta dal comportamento del rendering dell’anteprima: una presentazione autonoma semplifica la corrispondenza tra i due aspetti.

Esempio SwiftUI completo

Crea un’app macOS SwiftUI con una versione minima di macOS 13 o successiva. Sostituisci il punto di ingresso dell’app generato e la vista del contenuto con questo singolo file, lasciando esattamente una dichiarazione @main nel target. Il modificatore per l’anteprima personalizzata e l’overload della destinazione usati qui sono documentati come disponibili a partire da macOS 13. Consulta draggable(_:preview:) e dropDestination(for:action:isTargeted:).

import Foundation
import SwiftUI

@main
struct IntactDragDemoApp: App {
    var body: some Scene {
        WindowGroup {
            BoardView()
        }
    }
}

private struct CardFace: View {
    var body: some View {
        HStack(spacing: 14) {
            Circle()
                .fill(Color.orange)
                .frame(width: 36, height: 36)

            VStack(alignment: .leading, spacing: 6) {
                Text("Tile A")
                    .font(.headline)
                Text("Circle and label")
                    .font(.caption)
            }
            .foregroundStyle(Color.white)
        }
        .padding(16)
        .frame(width: 220, height: 100, alignment: .leading)
        .background(Color.indigo)
        .clipShape(RoundedRectangle(cornerRadius: 16))
    }
}

private struct BoardView: View {
    @State private var token = UUID().uuidString
    @State private var isOnRight = false

    var body: some View {
        VStack(spacing: 20) {
            Text("Drag the card between panels")
                .font(.headline)

            HStack(spacing: 24) {
                panel(right: false)
                panel(right: true)
            }
        }
        .padding(24)
    }

    private func panel(right: Bool) -> some View {
        VStack(spacing: 16) {
            Text(right ? "Right" : "Left")
                .font(.headline)

            ZStack {
                if isOnRight == right {
                    CardFace()
                        .contentShape(
                            .interaction,
                            RoundedRectangle(cornerRadius: 16)
                        )
                        .contentShape(
                            .dragPreview,
                            RoundedRectangle(cornerRadius: 16)
                        )
                        .draggable(token) {
                            CardFace()
                        }
                } else {
                    Text("Drop here")
                        .foregroundStyle(.secondary)
                }
            }
            .frame(width: 240, height: 120)
        }
        .frame(width: 260, height: 180)
        .background(
            RoundedRectangle(cornerRadius: 20)
                .fill(Color.gray.opacity(0.12))
        )
        .contentShape(Rectangle())
        .dropDestination(for: String.self) { items, _ in
            guard items == [token] else {
                return false
            }
            isOnRight = right
            return true
        }
    }
}

Questo esempio usa solo elementi grafici sintetici e un identificatore generato in memoria. String è già conforme a Transferable, quindi non richiede una rappresentazione di trasferimento personalizzata. Apple documenta questa conformità integrata in Trasformare una vista in un’origine di trascinamento.

L’interazione prevista è semplice: trascina Tile A nell’altro pannello, rilasciala, poi trascinala indietro. L’intera CardFace fornisce l’anteprima in movimento. La destinazione accetta esattamente un token corrispondente e modifica l’unico valore che indica la posizione. Restituire true segnala un rilascio riuscito; restituire false lo rifiuta, in accordo con il contratto documentato dell’azione di destinazione.

Questo è un esempio completo destinato a un target app macOS. Non è stato compilato né eseguito qui, perché l’ambiente di ricerca non dispone di una toolchain Swift né di un runtime macOS.

Separa la continuità visiva dalla proprietà del modello

Nell’esempio, la scheda originale resta visibile finché un rilascio accettato non modifica isOnRight. Rilasciarla fuori dai pannelli non modifica il modello. Rilasciarla di nuovo nel pannello già occupato riassegna la posizione esistente. Un token non corrispondente o più elementi vengono rifiutati prima di qualsiasi modifica dello stato.

Questo comportamento evita di rimuovere la scheda all’avvio del trascinamento per poi doverla ricostruire in caso di annullamento. L’anteprima garantisce la continuità visiva; la destinazione gestisce la modifica della posizione. Per una bacheca più ampia, applica lo stesso schema usando un identificatore stabile dell’elemento e un’unica raccolta autorevole.

La semantica dei valori di Swift aiuta a capire perché passare un payload non sposta automaticamente il modello originale. Strutture e stringhe hanno semantica di valore; assegnarle o passarle non ricolloca implicitamente un elemento nella raccolta dell’applicazione. L’aggiornamento del modello deve esprimere esplicitamente tale ricollocazione. Consulta Strutture e classi.

Distingui inoltre la ricollocazione a livello applicativo dall’operazione di trascinamento nativa. L’esempio modifica uno stato condiviso della bacheca dopo un rilascio accettato; non configura la negoziazione nativa dello spostamento né garantisce un cursore di spostamento.

Per macOS 26 e versioni successive, Apple documenta DragConfiguration per dichiarare le operazioni supportate e onDragSessionUpdated(_:) per osservare l’operazione eseguita. La destinazione sceglie l’operazione da eseguire. Se implementi la rimozione dalla sorgente dopo uno spostamento nativo, coordinati con la gestione della destinazione affinché lo stesso elemento condiviso non venga rimosso due volte. Consulta DragConfiguration e onDragSessionUpdated(_:).

Quando dovrebbe gestire AppKit la sessione di trascinamento?

Usa AppKit quando il criterio di accettazione include un’immagine di trascinamento posizionata esplicitamente, per esempio per mantenere il puntatore sullo stesso punto della scheda al momento della presa. Il modificatore documentato di SwiftUI per l’anteprima personalizzata accetta un payload e una vista di anteprima e specifica che l’anteprima è centrata. AppKit espone direttamente il riquadro dell’elemento trascinato.

Per un’implementazione con AppKit, segui questa sequenza:

  1. Lascia che la NSView sorgente riceva l’effettivo evento di pressione del mouse e tieni traccia del fatto che l’interazione diventi un trascinamento.
  2. Esegui il rendering dell’intera scheda sintetica in un unico NSImage e crea un solo NSDraggingItem con il relativo writer per la pasteboard.
  3. Imposta l’immagine e il suo riquadro iniziale con setDraggingFrame(_:contents:).
  4. Esprimi il riquadro nel sistema di coordinate della vista che avvia il trascinamento, quindi chiama beginDraggingSession(with:event:source:) con l’evento iniziale di pressione del mouse e il tuo NSDraggingSource.
  5. Lascia che la destinazione convalidi e confermi il rilascio. Usa il callback di completamento della sorgente per ripulire la sessione.

Apple specifica che la posizione del mouse dell’evento iniziale determina lo scostamento dell’icona trascinata e che il riquadro iniziale di ogni elemento deve usare le coordinate della vista sorgente. Mantieni il riquadro della scheda e la posizione dell’evento nello stesso sistema di coordinate. Consulta beginDraggingSession(with:event:source:).

setDraggingFrame(_:contents:) è il metodo di utilità documentato per un elemento trascinato rappresentato da una sola immagine. Una scheda sintetica può contenere diversi elementi disegnati pur essendo rappresentata da un’unica immagine. Consulta setDraggingFrame(_:contents:).

Preserva i confini degli eventi e delle sessioni

Scegli un solo iniziatore del trascinamento per la regione interattiva della scheda. Come regola di implementazione, evita di sovrapporre l’avvio di una sessione AppKit a un’origine draggable attiva di SwiftUI per la stessa regione. Entrambi cercherebbero di gestire l’interazione. Non generare eventi del mouse artificiali per compensare una gestione poco chiara dei gesti.

Puoi incorporare l’origine AppKit usando NSViewRepresentable. Mantieni la gestione degli eventi nativi nella sua NSView e comunica le modifiche al modello accettate tramite binding o un coordinatore. SwiftUI controlla il layout della vista rappresentata; Apple specifica di non impostare direttamente frame o bounds della vista gestita. Il riquadro dell’immagine di un elemento trascinato è una questione separata. Consulta NSViewRepresentable.

Non conservare un NSDraggingItem per modificarlo in seguito. Apple ne documenta la durata limitata: dopo la creazione della sessione, modifica gli elementi trascinati tramite enumerazione della sessione e usa ciascun elemento enumerato solo all’interno del relativo callback. Le modifiche apportate dalla destinazione possono anche sovrascrivere le modifiche all’immagine apportate dalla sorgente mentre il trascinamento si trova sopra quella destinazione. Consulta NSDraggingItem e enumerateDraggingItems(options:for:classes:searchOptions:using:).

Come si diagnostica un’anteprima che continua ad avere un aspetto errato?

Controlla in quale fase si presenta il problema:

Sintomo Primo controllo
Si spostano solo il cerchio o il testo Applica un’unica origine di trascinamento al contenitore dell’intera scheda.
Le dimensioni dell’anteprima differiscono da quelle della scheda Riutilizza la stessa vista di presentazione e dimensioni esplicite.
Il contorno al momento del sollevamento differisce dall’immagine in movimento Controlla separatamente .dragPreview e la closure dell’anteprima personalizzata.
Uno spazio vuoto avvia inaspettatamente il trascinamento Verifica .interaction e i limiti del contenitore sorgente.
L’immagine AppKit salta al momento della presa Controlla l’evento iniziale di pressione del mouse e le coordinate della vista sorgente.
Le modifiche successive all’immagine AppKit non hanno effetto Esegui le modifiche all’interno dell’enumerazione della sessione.
La scheda scompare dopo un annullamento Sposta la modifica del modello alla gestione del rilascio accettato e verifica la pulizia.

Verifica separatamente l’annullamento, un rilascio accettato, un rilascio di nuovo sul pannello originale e un payload rifiutato. L’invariante utile è che un’unica posizione del modello resti autorevole per tutta l’interazione. Inizia con l’anteprima completa di SwiftUI, poi adotta un’origine gestita da AppKit solo se il posizionamento richiesto dell’immagine richiede i controlli espliciti del riquadro e degli eventi.

Fonti