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:
- Permita que a
NSViewde origem receba o evento real de pressionamento do mouse e acompanhe se a interação se transforma em arrasto. - Renderize o cartão sintético completo em um único
NSImagee crie umNSDraggingItemcom seu gravador de área de transferência. - Defina a imagem e seu retângulo inicial com
setDraggingFrame(_:contents:). - 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 seuNSDraggingSource. - 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
draggable(_:preview:) — Documentação para desenvolvedores da Apple.
Transformar uma visualização em uma origem de arrasto — Documentação para desenvolvedores da Apple.
dragPreview — Documentação para desenvolvedores da Apple.
DragGesture — Documentação para desenvolvedores da Apple.
dropDestination(for:action:isTargeted:) — Documentação para desenvolvedores da Apple.
Estruturas e classes — Swift.org.
DragConfiguration — Documentação para desenvolvedores da Apple.
onDragSessionUpdated(_:) — Documentação para desenvolvedores da Apple.
beginDraggingSession(with:event:source:) — Documentação para desenvolvedores da Apple.
setDraggingFrame(_:contents:) — Documentação para desenvolvedores da Apple.
NSViewRepresentable — Documentação para desenvolvedores da Apple.
NSDraggingItem — Documentação para desenvolvedores da Apple.
enumerateDraggingItems(options:for:classes:searchOptions:using:) — Documentação para desenvolvedores da Apple.