Para uma forma arrastada dentro do seu app para Mac, atualize seu deslocamento diretamente durante o gesto e, em seguida, anime esse mesmo deslocamento com uma mola ao soltar. No macOS 14 e posteriores, o SwiftUI pode transferir automaticamente para a mola a velocidade rastreada durante o arrasto.

Este tutorial cria uma pequena interação para desktop: arraste um bloco azul sintético em qualquer direção, solte-o e deixe que ele volte à posição inicial como uma mola. Todo o estado fica na memória, e o gesto pertence ao bloco na própria janela do app.

Como a mola herda a velocidade final do arrasto?

Transaction.tracksVelocity do SwiftUI registra a velocidade conforme as propriedades animáveis mudam. A Apple documenta que os callbacks de gesto onChanged e updating ativam esse rastreamento automaticamente. Quando a interação termina, uma animação pode usar a velocidade acumulada. Portanto, você não precisa calcular e passar uma velocidade inicial para essa interação comum do SwiftUI. A documentação da Apple sobre rastreamento de velocidade descreve explicitamente essa transferência.

A conexão importante está na propriedade animada. Durante o arrasto, grave a translação do gesto no deslocamento do bloco. Ao soltar, altere esse mesmo deslocamento para o destino dentro de withAnimation. Manter a view e seu estado ativos dá ao SwiftUI uma propriedade contínua para rastrear.

Nesta implementação, mantenha as atualizações do arrasto sem animação. A Apple explica que o rastreamento de velocidade da transação e uma animação que gerencia a própria velocidade são mutuamente exclusivos. Uma mola interativa durante o arrasto é outra técnica compatível, mas altera como o bloco acompanha o ponteiro; ela não é necessária para a interação de acompanhamento direto apresentada aqui. Rastreamento de velocidade da transação

O destino da mola e seu movimento inicial têm funções diferentes. O destino indica onde o bloco deve parar. A velocidade herdada indica como ele está se movendo no momento em que é solto. Um bloco solto enquanto se afasta da posição inicial pode continuar se movendo para fora por um instante antes de a mola trazê-lo de volta. A Apple demonstra molas que dão continuidade ao movimento com base na velocidade do gesto em Animar com molas.

Crie um exemplo completo de arrastar e soltar no macOS

Crie um app para macOS no Xcode usando SwiftUI, defina o destino de implantação como macOS 14.0 ou posterior e substitua o arquivo de entrada do app gerado pelo código a seguir. Mantenha apenas uma declaração @main no destino. O exemplo usa a API de conclusão de animação do macOS 14. Documentação da Apple sobre conclusão de animações

Este é um app completo em um único arquivo, sem recursos ou dependências de pacotes. As assinaturas da API foram verificadas na documentação; o app para desktop não foi compilado nem executado neste ambiente de pesquisa.

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
        }
    }
}

O gesto é anexado ao bloco em movimento, enquanto seu espaço de coordenadas nomeado pertence à tela fixa. Isso deixa explícita a escolha do sistema de coordenadas da interação. A documentação da Apple sobre DragGesture descreve como anexar um arrasto configurado com o modificador de gesto.

value.translation é o deslocamento total desde o início do arrasto. Atribua-o a offset; não o some repetidamente ao deslocamento anterior. A soma repetida acumularia várias vezes o mesmo movimento. Neste exemplo, todo arrasto aceito começa na posição inicial, então não é necessário um deslocamento inicial separado. Definição de translação da Apple

Ao soltar com sucesso, chama-se returnHome() imediatamente. O deslocamento não é redefinido antes, nem a mola é agendada para depois. Assim, as mudanças rastreadas durante o arrasto e a animação ao soltar permanecem conectadas pela mesma propriedade de estado.

Por que manter estados separados para o arrasto, a posição e o retorno?

As três propriedades respondem a perguntas diferentes: offset armazena o deslocamento solicitado, isDragging acompanha um gesto ativo e isReturning registra a fase de retorno do exemplo. O proprietário do bloco mantém esses valores localmente. A Apple recomenda declarar como privadas as propriedades de estado pertencentes à view e compartilhar bindings apenas quando outra view precisar alterá-las. Documentação sobre State

A sintaxe do wrapper também tem uma função específica. Os wrappers de propriedade do Swift gerenciam o acesso a um valor encapsulado e podem expor um valor projetado com o prefixo de cifrão. Aqui, $isDragging fornece a projeção do estado do gesto usada por updating; não é uma segunda flag mantida de forma independente. A linguagem de programação Swift: propriedades

O cancelamento precisa de um tratamento separado porque onEnded só é executado quando um gesto termina com sucesso. A Apple recomenda @GestureState para estados que devem ser redefinidos independentemente de como o gesto termina. O exemplo observa essa redefinição e restaura o bloco sintético se nenhum retorno normal estiver em andamento. Essa alternativa faz o bloco voltar à posição inicial instantaneamente; ela não afirma que exista uma velocidade final de soltura para um gesto cancelado. Comportamento ao concluir gestos, GestureState

O exemplo também aceita deliberadamente um novo arrasto somente depois que a animação de retorno termina. Definir o deslocamento do modelo como zero inicia a animação, então zero já é o destino solicitado enquanto o bloco ainda está visivelmente em movimento. Remover essa restrição exigiria projetar como um novo arrasto assumiria o controle a partir da posição exibida naquele momento.

allowsHitTesting(!isReturning) controla se o bloco participa do teste de acerto. O critério de conclusão .removed libera a restrição quando a animação inteira termina. Isso evita adivinhar um atraso com base no parâmetro de duração da mola. Controle do teste de acerto, Conclusão por remoção da animação

Como ajustar e verificar o retorno?

Comece com um destino fixo e configurações fixas para a mola. Mude a maneira como solta o bloco antes de alterar os parâmetros da animação. Isso permite distinguir a herança de velocidade do ajuste da mola.

Faça as verificações a seguir dentro da janela. Esta é uma sequência de diagnóstico baseada na transferência documentada, não em resultados medidos neste exemplo.

Verificação Ação O que observar
Soltura quase sem movimento Arraste para longe da posição inicial, faça uma pausa e solte. O retorno deve começar com pouco movimento herdado.
Soltura para fora Arraste para longe da posição inicial e solte enquanto ainda se move para fora. A mola deve manter o movimento para fora antes de inverter a direção.
Soltura para dentro Arraste para longe, inverta a direção rumo à posição inicial e solte. O retorno deve dar continuidade ao movimento para dentro.
Soltura diagonal Solte enquanto se move pelos dois eixos. Verifique se a transferência mantém a direção, em vez de se comportar como um retorno apenas horizontal.
Próxima interação Aguarde o fim do retorno e arraste novamente. O novo arrasto deve começar na posição inicial.

Ajuste duration para controlar o ritmo e bounce para definir o caráter do movimento. 0.45 e 0.2 do exemplo são valores iniciais ilustrativos, não configurações obrigatórias. A Apple define a duração como duração perceptual e descreve o valor zero de elasticidade como criticamente amortecido; valores positivos de elasticidade produzem molas progressivamente mais oscilantes. Configuração da mola

A duração perceptual não garante exatamente quando todo movimento visível termina. A Apple a distingue da duração de acomodação, que depende do movimento da mola. Para a fase de retorno deste exemplo, use o callback de conclusão em vez de tratar 0.45 como um temporizador. Animar com molas

Se parecer que o movimento é ignorado ao soltar, inspecione primeiro a transferência: o gesto está alterando o mesmo deslocamento que a mola anima? Algum outro código sobrescreve esse deslocamento antes da soltura? A view está sendo substituída? Um modificador de animação está animando as atualizações do arrasto? Essas verificações decorrem diretamente do requisito documentado de rastrear propriedades animáveis em mudança antes de passá-las a uma animação.

Quando você deve ler a velocidade explicitamente ou usar AppKit?

DragGesture.Value.velocity expõe a velocidade atual do arrasto, e onEnded fornece o valor final do gesto após uma conclusão bem-sucedida. Ler esse valor é útil quando a velocidade influencia uma decisão do app, como escolher entre dois pontos de encaixe sintéticos. Para o destino fixo de retorno deste exemplo, o rastreamento automático já fornece a transferência para a animação. Velocidade do arrasto, Valor final do gesto

Se você escolher uma API explícita de velocidade inicial para a mola, confira sua representação antes de passar uma medição do gesto. A demonstração da Apple de animação controlada por gesto converte a velocidade em velocidade unitária, dividindo-a pela distância restante até o destino. Nesse cálculo ilustrativo, 300 pontos por segundo ao longo de 150 pontos de deslocamento se convertem em 2 distâncias de animação por segundo. Essa conversão também exige uma política deliberada quando a distância restante é zero. Neste caso, o caminho automático do SwiftUI evita esse cálculo. Aprimore suas animações e transições de UI

Use AppKit quando uma view ou um reconhecedor existente, pertencente ao app e baseado em AppKit, precisar controlar a interação. NSPanGestureRecognizer.velocity(in:) retorna as velocidades horizontal e vertical em pontos por segundo em relação à view fornecida. Embora a assinatura Swift aceite uma view opcional, a Apple diz explicitamente que esse argumento não pode ser nil. Velocidade de pan do AppKit

No macOS 26 e posteriores, NSGestureRecognizerRepresentable é a forma documentada de integrar um reconhecedor de gestos do AppKit a uma hierarquia SwiftUI. O SwiftUI gerencia sua criação e suas atualizações, além de fornecer suporte à conversão de coordenadas. A Apple alerta que a geometria da view subjacente ao reconhecedor pode diferir da geometria da view SwiftUI; ao cruzar esse limite, use o conversor do representable. Integração de gestos do AppKit

Para a interação com o bloco, mantenha o gesto SwiftUI como o único responsável pelas atualizações do arrasto e deixe que a mola de retorno anime o deslocamento existente. Quando essa transferência funcionar, ajuste o destino e os parâmetros da mola, preservando o mesmo estado e os mesmos limites do sistema de coordenadas.

Fontes