Per trascinare una forma nella tua app Mac, aggiorna direttamente il suo offset durante il gesto, quindi anima lo stesso offset con una molla al rilascio. Da macOS 14 in poi, SwiftUI può trasferire automaticamente alla molla la velocità rilevata durante il trascinamento.

Questo tutorial realizza una piccola interazione desktop: trascina in qualsiasi direzione una tessera blu sintetica, rilasciala e lascia che torni alla posizione iniziale con un movimento a molla. Tutto lo stato resta in memoria e il gesto appartiene alla tessera nella finestra dell’app.

In che modo la molla eredita la velocità finale del trascinamento?

Transaction.tracksVelocity di SwiftUI registra la velocità mentre cambiano le proprietà animabili. Apple documenta che i callback onChanged e updating dei gesti abilitano automaticamente questo tracciamento. Al termine dell’interazione, un’animazione può utilizzare la velocità accumulata. Per questa normale interazione SwiftUI, quindi, non occorre calcolare e passare una velocità iniziale. La documentazione Apple sul tracciamento della velocità descrive esplicitamente questo passaggio.

Il collegamento fondamentale è la proprietà animata. Durante il trascinamento, assegna la traslazione del gesto all’offset della tessera. Al rilascio, modifica quello stesso offset impostandone la destinazione all’interno di withAnimation. Mantenere attivi la vista e il suo stato consente a SwiftUI di tracciare una proprietà continua.

In questa implementazione, gli aggiornamenti durante il trascinamento non sono animati. Apple spiega che il tracciamento della velocità della transazione e un’animazione che gestisce autonomamente la propria velocità si escludono a vicenda. Durante il trascinamento è possibile anche usare una molla interattiva, ma cambia il modo in cui la tessera segue il puntatore; non è necessaria per l’interazione in cui la tessera lo segue direttamente, illustrata qui. Tracciamento della velocità della transazione

La destinazione di una molla e il suo moto iniziale hanno funzioni diverse. La destinazione indica dove deve fermarsi la tessera. La velocità ereditata indica come si muove al momento del rilascio. Se la tessera viene rilasciata mentre si allontana dalla posizione iniziale, può continuare inizialmente verso l’esterno prima che la molla la riporti indietro. Apple mostra come le molle possano proseguire il movimento in base alla velocità del gesto in Animare con le molle.

Crea un esempio macOS completo di trascinamento e rilascio

In Xcode crea un’app macOS con SwiftUI, imposta il target di distribuzione su macOS 14.0 o successivo e sostituisci il file di ingresso dell’app generato con il codice seguente. Nel target deve esserci una sola dichiarazione @main dell’app. L’esempio usa l’API di macOS 14 per il completamento delle animazioni. La documentazione Apple sul completamento delle animazioni

Questa è un’applicazione completa in un unico file, senza risorse né dipendenze da pacchetti. Le firme delle API sono state verificate nella documentazione; in questo ambiente di ricerca l’app desktop non è stata compilata né eseguita.

import SwiftUI

@main
struct SpringReleaseDemoApp: App {
    var body: some Scene {
        WindowGroup {
            SpringReleaseView()
        }
    }
}

struct SpringReleaseView: View {
    @State private var offset: CGSize = .zero
    @State private var isReturning = false
    @GestureState private var isDragging = false

    var body: some View {
        VStack(spacing: 16) {
            Text("Drag the tile, then release")
                .font(.headline)

            ZStack {
                RoundedRectangle(cornerRadius: 20)
                    .stroke(.gray.opacity(0.4), lineWidth: 2)
                    .frame(width: 112, height: 112)

                RoundedRectangle(cornerRadius: 20)
                    .fill(.blue)
                    .frame(width: 112, height: 112)
                    .contentShape(Rectangle())
                    .offset(offset)
                    .gesture(tileDrag)
                    .allowsHitTesting(!isReturning)
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .coordinateSpace(name: "canvas")
        }
        .padding(24)
        .frame(width: 640, height: 440)
        .onChange(of: isDragging) { _, active in
            guard !active, !isReturning else { return }

            // Cancellation cleanup has no successful release value.
            withTransaction(Transaction(animation: nil)) {
                offset = .zero
            }
        }
    }

    private var tileDrag: some Gesture {
        DragGesture(
            minimumDistance: 3,
            coordinateSpace: .named("canvas")
        )
        .updating($isDragging) { _, active, _ in
            active = true
        }
        .onChanged { value in
            // Gesture callbacks automatically track velocity.
            offset = value.translation
        }
        .onEnded { _ in
            returnHome()
        }
    }

    private func returnHome() {
        guard !isReturning else { return }
        isReturning = true

        withAnimation(
            .spring(duration: 0.45, bounce: 0.2),
            completionCriteria: .removed
        ) {
            offset = .zero
        } completion: {
            isReturning = false
        }
    }
}

Il gesto è collegato alla tessera in movimento, mentre lo spazio di coordinate con nome appartiene alla tela immobile. In questo modo la scelta delle coordinate dell’interazione è esplicita. La documentazione Apple di DragGesture spiega come collegare un trascinamento configurato usando il modificatore del gesto.

value.translation è lo spostamento totale dall’inizio del trascinamento. Assegnalo a offset; non sommarlo ripetutamente all’offset precedente. Sommarlo ogni volta accumulerebbe più volte lo stesso movimento. In questo esempio ogni trascinamento accettato parte dalla posizione iniziale, quindi non serve un offset iniziale separato. La definizione di traslazione di Apple

Al rilascio riuscito, viene chiamato subito returnHome(). L’offset non viene prima azzerato né la molla viene programmata per un momento successivo. In questo modo le modifiche tracciate durante il trascinamento e l’animazione al rilascio restano collegate tramite la stessa proprietà di stato.

Perché mantenere separati lo stato del trascinamento, della posizione e del ritorno?

Le tre proprietà rispondono a domande diverse: offset memorizza lo spostamento richiesto, isDragging indica se è attivo un gesto e isReturning registra la fase di ritorno dell’esempio. Il proprietario della tessera conserva questi valori localmente. Apple consiglia di dichiarare come privato lo stato posseduto da una vista e di condividere binding solo quando un’altra vista deve poterlo modificare. Documentazione sullo stato

Anche la sintassi dei wrapper ha uno scopo preciso. I wrapper di proprietà Swift gestiscono l’accesso a un valore racchiuso e possono esporre un valore proiettato tramite il prefisso con il simbolo del dollaro. Qui $isDragging fornisce la proiezione dello stato del gesto usata da updating; non è un secondo flag gestito indipendentemente. Il linguaggio di programmazione Swift: proprietà

La cancellazione richiede una gestione separata perché onEnded viene eseguito solo quando un gesto termina correttamente. Apple consiglia @GestureState per lo stato che deve essere ripristinato indipendentemente da come termina un gesto. L’esempio rileva questo ripristino e riporta la tessera sintetica alla posizione iniziale se non è già in corso un normale ritorno. Questa soluzione di ripiego la riporta di scatto alla posizione iniziale; non presuppone di conoscere la velocità finale di rilascio di un gesto annullato. Comportamento al completamento del gesto, GestureState

Inoltre, l’esempio accetta intenzionalmente un nuovo trascinamento solo dopo la conclusione dell’animazione di ritorno. L’impostazione dell’offset del modello a zero avvia l’animazione: quindi zero è già la destinazione richiesta mentre la tessera è ancora visibilmente in movimento. Per rimuovere semplicemente questa limitazione occorrerebbe decidere come un nuovo trascinamento debba subentrare dalla posizione attualmente visualizzata.

allowsHitTesting(!isReturning) determina se la tessera partecipa al rilevamento dei tocchi. Il criterio di completamento .removed elimina la limitazione quando termina l’intera animazione. Così non occorre stimare un ritardo basandosi sul parametro di durata della molla. Controllo del rilevamento dei tocchi, Completamento alla rimozione dell’animazione

Come regolare e verificare il rilascio?

Inizia con una destinazione e impostazioni della molla fisse. Cambia il modo in cui rilasci la tessera prima di modificare i parametri dell’animazione. Così puoi distinguere l’ereditarietà della velocità dalla regolazione della molla.

Esegui le seguenti verifiche nella finestra. Sono una sequenza diagnostica ricavata dal passaggio documentato, non risultati misurati con questo esempio.

Verifica Azione Cosa osservare
Rilascio quasi da ferma Trascina la tessera lontano dalla posizione iniziale, fai una pausa e rilasciala. Il ritorno dovrebbe iniziare con poco movimento trasferito.
Rilascio verso l’esterno Trascina la tessera lontano dalla posizione iniziale e rilasciala mentre si muove ancora verso l’esterno. La molla dovrebbe mantenere il moto verso l’esterno prima di invertire la direzione.
Rilascio verso l’interno Trascina la tessera lontano dalla posizione iniziale, inverti la direzione verso la posizione iniziale e rilasciala. Il ritorno dovrebbe proseguire il movimento verso l’interno.
Rilascio diagonale Rilasciala mentre si muove su entrambi gli assi. Verifica che il passaggio mantenga la direzione, invece di comportarsi come un ritorno solo orizzontale.
Interazione successiva Attendi la fine del ritorno, quindi trascina di nuovo. Il nuovo trascinamento dovrebbe partire dalla posizione iniziale.

Regola duration per il ritmo e bounce per il carattere. I valori 0.45 e 0.2 dell’esempio sono valori iniziali illustrativi, non impostazioni consigliate. Apple definisce la durata come durata percettiva e descrive il rimbalzo zero come smorzato criticamente; valori di rimbalzo positivi generano molle sempre più rimbalzanti. Configurazione della molla

La durata percettiva non garantisce esattamente quando terminerà ogni movimento visibile. Apple la distingue dalla durata di assestamento, che dipende dal movimento della molla. Per la fase di ritorno dell’esempio usa il callback di completamento invece di trattare 0.45 come un timer. Animare con le molle

Se al rilascio il movimento sembra venire ignorato, controlla innanzitutto il passaggio: il gesto modifica lo stesso offset che la molla anima? Altro codice sovrascrive quell’offset prima del rilascio? La vista viene sostituita? Un modificatore di animazione anima gli aggiornamenti durante il trascinamento? Queste verifiche derivano direttamente dal requisito documentato di tracciare le proprietà animabili mentre cambiano prima di passarle a un’animazione.

Quando leggere esplicitamente la velocità o usare AppKit?

DragGesture.Value.velocity espone la velocità corrente del trascinamento e onEnded fornisce il valore finale del gesto dopo un completamento riuscito. Leggerla è utile quando la velocità influisce su una decisione dell’applicazione, per esempio la scelta tra due posizioni di aggancio sintetiche. Per la destinazione di ritorno fissa di questo esempio, il tracciamento automatico fornisce già il passaggio all’animazione. Velocità del trascinamento, Valore finale del gesto

Se scegli un’API esplicita per la velocità iniziale della molla, verifica la rappresentazione prima di passarle una misura del gesto. La dimostrazione di Apple sulle animazioni guidate dai gesti converte la velocità in velocità unitaria dividendola per la distanza di percorrenza rimanente. Con i valori illustrativi, questo calcolo trasformerebbe 300 punti al secondo su una distanza rimanente di 150 punti in 2 distanze di animazione al secondo. La conversione richiede anche una strategia precisa quando la distanza rimanente è zero. In questo caso, il percorso automatico di SwiftUI evita il calcolo. Migliorare le animazioni e le transizioni dell’interfaccia utente

Usa AppKit quando l’interazione deve essere gestita da una vista o da un riconoscitore AppKit già posseduto dall’app. NSPanGestureRecognizer.velocity(in:) restituisce la velocità orizzontale e verticale in punti al secondo rispetto alla vista fornita. Sebbene la firma Swift accetti una vista opzionale, Apple specifica esplicitamente che questo argomento non deve essere nil. Velocità del pan in AppKit

Da macOS 26 in poi, NSGestureRecognizerRepresentable è il metodo documentato per integrare un riconoscitore di gesti AppKit in una gerarchia SwiftUI. SwiftUI ne gestisce la creazione e gli aggiornamenti e fornisce il supporto per la conversione delle coordinate. Apple avverte che la geometria della vista sottostante del riconoscitore potrebbe differire da quella della vista SwiftUI; quando si attraversa questo confine, usa quindi il convertitore di coordinate del representable. Integrazione dei gesti AppKit

Per l’interazione con la tessera, mantieni il gesto SwiftUI come unico responsabile degli aggiornamenti del trascinamento e lascia che la molla al rilascio animi l’offset già esistente. Quando il passaggio funziona, regola la destinazione e i parametri della molla mantenendo gli stessi confini di stato e coordinate.

Fonti