Para fazer um cartão sintético parecer intacto durante um arrasto nativo do macOS, atribua uma única origem de arrasto ao contêiner externo e renderize o cartão completo como prévia. Use o AppKit quando precisar controlar explicitamente o quadro inicial da prévia e o deslocamento do ponteiro.

Este guia aborda o arrasto de um cartão em memória entre dois painéis no seu próprio app para Mac. O objetivo prático é preservar as formas, o texto, o espaçamento e a identidade do cartão, mantendo o tratamento dos gestos e as atualizações do modelo sob controle bem definido.

Que parte do arrasto precisa mudar?

A Apple separa os dados transferidos da representação visual exibida durante o arrasto. O draggable(_:preview:) do SwiftUI gera uma renderização da sua visualização de prévia quando o arrasto começa. A documentação especifica que a prévia fica centralizada sobre a visualização de origem. Ela não transfere a própria visualização de origem. Consulte a documentação do draggable(_:preview:) da Apple.

Use este guia de decisão para identificar o controle de que você realmente precisa:

Comportamento desejado Ponto de partida compatível
Manter juntos o rótulo, as formas e o fundo do cartão Uma única origem de arrasto externa com uma prévia personalizada completa
Ajustar o contorno durante a transição de levantamento contentShape(.dragPreview, shape)
Alterar qual parte do cartão pode iniciar o arrasto contentShape(.interaction, shape)
Confirmar a transferência de um cartão para outro painel Validar a carga útil e atualizar o modelo no destino
Definir explicitamente o retângulo inicial da imagem de arrasto e sua relação com o ponteiro Uma sessão controlada pelo AppKit usando NSDraggingItem
Mover um elemento continuamente em uma tela sem destinos de soltura nativos Uma interação local com DragGesture

A distinção entre as duas formas de conteúdo é importante: a Apple documenta que .dragPreview afeta a forma da prévia, enquanto .interaction controla o teste de acerto. Alterar apenas o contorno da prévia não amplia a região que inicia o arrasto. Consulte ContentShapeKinds.dragPreview.

Um DragGesture local comunica um movimento de arrasto por meio de uma sequência de gestos. Escolha essa abordagem quando a tarefa for atualizar a posição em uma tela. Para a tarefa nativa de soltura entre painéis deste artigo, deixe que draggable instale os gestos de arrasto. Consulte a referência de DragGesture e o guia de origem de arrasto da Apple.

Crie uma única prévia para o cartão sintético inteiro

Aplique o modificador de arrasto ao menor contêiner que inclua tudo o que deve se mover junto. Para um cartão composto por um círculo, duas linhas de texto e um fundo arredondado, esse contêiner é o cartão completo. Atribuir origens de arrasto separadas aos elementos filhos descreveria interações separadas.

Reutilize uma única visualização de apresentação para a origem e a prévia. Defina dimensões explícitas para essa visualização e inclua nela o próprio fundo, em vez de depender de estilos herdados do painel ao redor. Esta é uma recomendação de projeto baseada no comportamento da prévia renderizada: uma apresentação autocontida facilita combinar as duas aparências.

Exemplo completo em SwiftUI

Crie um app SwiftUI para macOS com destino de implantação macOS 13 ou posterior. Substitua a entrada do app e a visualização de conteúdo geradas por este único arquivo, mantendo exatamente uma declaração @main no destino. Tanto o modificador de prévia personalizada quanto a sobrecarga de destino usados aqui estão documentados como disponíveis a partir do macOS 13. Consulte 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
        }
    }
}

Este exemplo usa apenas elementos gráficos sintéticos e um identificador gerado em memória. String já está em conformidade com Transferable, então não precisa de uma representação de transferência personalizada. A Apple documenta essa conformidade integrada em Transformar uma visualização em uma origem de arrasto.

A interação esperada é simples: arraste Tile A para o outro painel, solte-o e depois arraste-o de volta. O CardFace completo fornece a prévia em movimento. O destino aceita exatamente um token correspondente e altera o único valor de localização. Retornar true informa que a soltura foi bem-sucedida; retornar false a rejeita, conforme o contrato documentado da ação de destino.

Este é um exemplo completo destinado a um app para macOS. Ele não foi compilado nem executado aqui, pois o ambiente de pesquisa não tem uma cadeia de ferramentas Swift nem um ambiente de execução do macOS.

Mantenha a continuidade visual separada da propriedade do modelo

No exemplo, o cartão original permanece visível até que uma soltura aceita altere isOnRight. Soltar fora dos painéis não modifica o modelo. Soltar novamente no painel já ocupado atribui a localização existente. Um token incompatível ou vários itens são rejeitados antes que o estado mude.

Esse comportamento evita remover o cartão no início do arrasto e depois ter de recriá-lo em caso de cancelamento. A prévia mantém a continuidade visual; o destino é responsável pela alteração da localização. Em um tabuleiro maior, aplique o mesmo padrão a um identificador estável de item e a uma única coleção autoritativa.

A semântica de valor do Swift ajuda a explicar por que passar uma carga útil não move automaticamente o modelo original. Estruturas e strings têm semântica de valor; atribuí-las ou passá-las não realoca implicitamente um item na coleção do seu app. A atualização do modelo precisa expressar essa realocação. Consulte Estruturas e classes.

Também diferencie uma realocação no nível do app da operação de arrasto nativa. O exemplo altera um estado compartilhado do tabuleiro após uma soltura aceita; ele não configura a negociação de movimentação nativa nem garante um cursor de movimentação.

No macOS 26 e posterior, a Apple documenta DragConfiguration para declarar as operações compatíveis e onDragSessionUpdated(_:) para observar a operação realizada. Um destino escolhe a operação que executa. Se você implementar a remoção na origem após uma movimentação nativa, coordene-a com o tratamento no destino para que o mesmo item compartilhado não seja removido duas vezes. Consulte DragConfiguration e onDragSessionUpdated(_:).

Quando o AppKit deve controlar a sessão de arrasto?

Use o AppKit quando o critério de aceitação incluir uma imagem de arrasto posicionada explicitamente, como manter o ponteiro sobre o mesmo ponto dentro do cartão no momento em que ele é pego. O modificador documentado de prévia personalizada do SwiftUI recebe uma carga útil e uma visualização de prévia, e descreve uma prévia centralizada. O AppKit expõe diretamente o quadro do item de arrasto.

Para uma implementação com AppKit, siga esta sequência:

  1. Permita que a NSView de origem receba o evento real de pressionamento do mouse e acompanhe se a interação se transforma em arrasto.
  2. Renderize o cartão sintético completo em um único NSImage e crie um NSDraggingItem com seu gravador de área de transferência.
  3. Defina a imagem e seu retângulo inicial com setDraggingFrame(_:contents:).
  4. Expresse esse retângulo no sistema de coordenadas da visualização que iniciou a ação e, em seguida, chame beginDraggingSession(with:event:source:) com o evento inicial de pressionamento do mouse e seu NSDraggingSource.
  5. Deixe o destino validar e confirmar a soltura. Use o retorno de chamada de conclusão da origem para limpar a sessão.

A Apple especifica que a localização do mouse no evento inicial determina o deslocamento do ícone arrastado e que o quadro inicial de cada item deve usar as coordenadas da visualização de origem. Mantenha o retângulo do cartão e a localização do evento no mesmo sistema de coordenadas. Consulte beginDraggingSession(with:event:source:).

setDraggingFrame(_:contents:) é o método de conveniência documentado para um item de arrasto representado por uma única imagem. Um cartão sintético pode conter vários elementos desenhados e ainda ser representado por essa única imagem. Consulte setDraggingFrame(_:contents:).

Preserve os limites dos eventos e da sessão

Escolha um único iniciador de arrasto para a região interativa do cartão. Como regra de implementação, evite sobrepor um iniciador de sessão do AppKit a uma origem ativa draggable do SwiftUI para a mesma região. Ambos tentariam controlar a interação. Não crie eventos de mouse artificialmente para compensar uma propriedade dos gestos pouco clara.

Você pode incorporar a origem do AppKit usando NSViewRepresentable. Mantenha o tratamento de eventos nativos na NSView e comunique as alterações aceitas no modelo por meio de bindings ou de um coordenador. O SwiftUI controla o layout da visualização representada; a Apple especifica que não se deve definir diretamente frame nem bounds nessa visualização gerenciada. O retângulo da imagem de um item de arrasto é uma questão separada. Consulte NSViewRepresentable.

Não retenha um NSDraggingItem para modificá-lo posteriormente. A Apple documenta sua vida útil limitada: após a criação da sessão, altere os itens de arrasto por meio da enumeração da sessão e use cada item enumerado apenas dentro do respectivo retorno de chamada. Alterações feitas no destino também podem substituir alterações de imagem feitas na origem enquanto o arrasto está sobre esse destino. Consulte NSDraggingItem e enumerateDraggingItems(options:for:classes:searchOptions:using:).

Como diagnosticar uma prévia que ainda parece errada?

Verifique a falha no estágio em que ela aparece:

Sintoma Primeira verificação
Apenas o círculo ou o texto se move Coloque uma única origem de arrasto no contêiner completo do cartão.
As dimensões da prévia diferem das do cartão Reutilize a mesma visualização de apresentação e dimensões explícitas.
O contorno ao pegar o cartão difere da imagem em movimento Verifique .dragPreview separadamente do fechamento da prévia personalizada.
Um espaço vazio inicia o arrasto inesperadamente Revise .interaction e os limites do contêiner de origem.
A imagem do AppKit salta ao pegar o cartão Verifique o evento inicial de pressionamento do mouse e as coordenadas da visualização de origem.
Alterações posteriores na imagem do AppKit não surtem efeito Faça-as dentro da enumeração da sessão.
O cartão desaparece após o cancelamento Mova a mutação do modelo para o tratamento de solturas aceitas e revise a limpeza.

Verifique separadamente o cancelamento, uma soltura aceita, uma soltura de volta no painel original e uma carga útil rejeitada. A invariável importante é que uma única localização do modelo permaneça autoritativa o tempo todo. Comece com a prévia completa em SwiftUI e adote uma origem controlada pelo AppKit apenas quando o posicionamento exigido da imagem precisar do controle explícito do quadro e dos eventos.

Fontes