Restaure formas esmaecidas, contornos e rótulos de arraste no callback de encerramento da sessão da origem do arraste. O callback de saída de um destino significa apenas que o ponteiro deixou aquele destino. Em um app para Mac, use a API de sessão da origem do SwiftUI quando disponível ou deixe uma origem do AppKit controlar o arraste.
Este guia aborda uma tarefa: devolver uma forma sintética à aparência normal depois que o usuário termina de arrastá-la, inclusive quando nenhum destino a aceita. O exemplo mantém seu conteúdo na memória e trata uma soltura aceita como uma cópia, portanto restaurar o feedback nunca exige excluir nem mover a origem.
Por que o feedback de arraste permanece ativo fora dos destinos de soltura?
Uma implementação comum define um sinalizador isDragging quando o arraste começa e o limpa em performDrop. Isso vincula a limpeza a um evento que apenas um destino que aceite a soltura pode tratar. Soltar sobre um espaço vazio da janela não deixa nenhum destino responsável por redefinir a origem.
A Apple define DropDelegate como uma interface para uma view modificada para aceitar solturas. Seu método dropExited(info:) informa que um arraste validado saiu dessa view. O arraste pode continuar até outro destino, voltar ao primeiro ou terminar em outro lugar. Portanto, a saída de um destino não permite concluir que a sessão da origem terminou. DropDelegate
A mesma distinção se aplica a dropDestination(for:action:isTargeted:): isTargeted indica se o ponteiro está dentro da área do destino. Use-o para a borda ou o fundo do destino. Deixe que um callback da sessão da origem controle o esmaecimento da origem ou o rótulo de arraste. dropDestination(for:action:isTargeted:)
Uma regra de projeto útil decorre desses limites documentados: atribua cada estado visual ao responsável pelo evento que pode encerrá-lo de forma confiável.
| Feedback ou resultado | Responsável | Ponto de redefinição ou confirmação |
|---|---|---|
| Forma de origem esmaecida | Origem do arraste | A sessão da origem termina |
| Destino destacado | Destino da soltura | O ponteiro sai; para um arraste local, faça uma limpeza preventiva ao concluir na origem |
| Item sintético aceito | Destino/modelo | A soltura é validada e tem êxito |
| Recursos temporários exportados | Ciclo de vida da transferência | Conclusão da transferência, quando aplicável |
Qual abordagem compatível um app para Mac deve usar?
A referência atual da Apple lista onDragSessionUpdated(_:) como disponível no macOS 26 e posteriores. Ele observa atualizações da sessão de arraste mais próxima na hierarquia de views filhas. Anexe-o diretamente fora do modificador de arraste da origem para deixar claro seu escopo. Use um SDK que realmente declare o método; a disponibilidade em tempo de execução não faz uma declaração ausente no SDK compilar. onDragSessionUpdated(_:)
Há uma divergência na documentação que convém tratar de forma restrita: o exemplo do modificador mostra uma fase encerrada com argumentos de localização e operação, enquanto a referência dedicada da fase declara ended(DropOperation). O exemplo abaixo verifica se a fase corresponde a .ended, sem extrair valores associados, pois a restauração visual não precisa deles. Consulte a declaração do SDK instalado antes de adicionar comportamento específico para operações. DragSession.Phase.ended(_:)
Se o destino de implantação ou o SDK não puder usar o callback do SwiftUI, uma origem de arraste do AppKit oferece o limite de conclusão documentado. A Apple afirma explicitamente que uma origem recebe draggingSession(_:endedAt:operation:) quando seu arraste termina ou é cancelado. Uma origem personalizada precisa iniciar essa sessão. beginDraggingSession(with:event:source:)
Se você só precisa reposicionar uma forma dentro de uma única tela, considere usar um DragGesture local em vez de uma sessão do sistema de arrastar e soltar. @GestureState é redefinido quando o gesto fica inativo, sendo apropriado para o feedback temporário desse gesto. Mantenha essa opção separada de um arraste do sistema que fornece conteúdo transferível aos destinos. GestureState
Implemente a limpeza controlada pela origem no SwiftUI
Crie um app macOS com SwiftUI usando um SDK que exponha onDragSessionUpdated(_:), defina o destino de implantação como macOS 26 ou posterior e substitua as declarações geradas do app e da view de conteúdo por este programa completo. Ele fornece uma única string sintética, shape.circle, e uma área receptora.
A origem usa draggable(_:) para fornecer conteúdo transferível. O destino aceita exatamente uma string correspondente e retorna false para outras entradas. A Apple documenta que o resultado booleano do destino indica se a soltura teve êxito. draggable(_:), dropDestination(for:action:isTargeted:)
import SwiftUI
@main
struct DragFeedbackDemoApp: App {
var body: some Scene {
WindowGroup {
DragFeedbackDemo()
}
}
}
struct DragFeedbackDemo: View {
private let payload = "shape.circle"
@State private var isDragging = false
@State private var isTargeted = false
@State private var acceptedCopies = 0
var body: some View {
VStack(spacing: 24) {
Text("Drag the circle into the receiving area")
Circle()
.fill(Color.blue)
.frame(width: 72, height: 72)
.opacity(isDragging ? 0.4 : 1)
.draggable(payload)
.onDragSessionUpdated { session in
switch session.phase {
case .initial, .active:
isDragging = true
case .ended, .dataTransferCompleted:
restoreFeedback()
default:
break
}
}
Text(isDragging ? "Dragging" : "Ready")
RoundedRectangle(cornerRadius: 12)
.fill(isTargeted
? Color.blue.opacity(0.2)
: Color.gray.opacity(0.12))
.overlay {
Text("Receiving area")
}
.frame(width: 280, height: 120)
.dropDestination(for: String.self) { items, _ in
guard items.count == 1,
items.first == payload else {
return false
}
acceptedCopies += 1
return true
} isTargeted: { targeted in
isTargeted = targeted
}
Text("Accepted copies: \(acceptedCopies)")
}
.padding(32)
.frame(minWidth: 380, minHeight: 400)
}
private func restoreFeedback() {
isDragging = false
isTargeted = false
}
}
Os dois sinalizadores têm funções diferentes. isDragging altera a opacidade da origem. isTargeted altera o preenchimento da área receptora. Assim, entrar e sair da área receptora não pode restaurar acidentalmente a origem enquanto o arraste continua.
A referência da fase identifica initial como a sessão prestes a começar, active como movimento e ended como conclusão. O exemplo restaura o feedback em ended; também repete a mesma redefinição em dataTransferCompleted, sem efeitos indesejados. Ele nunca depende exclusivamente da conclusão da transferência para restaurar um arraste malsucedido. DragSession.Phase
A Apple descreve dataTransferCompleted como o momento em que os itens arrastados foram transferidos e os itens temporários podem ser limpos. Este exemplo não tem recursos temporários de exportação. Em uma implementação maior, mantenha a limpeza visual separada da limpeza de recursos para que restaurar a opacidade não descarte algo de que uma transferência ainda precise. DragSession.Phase.dataTransferCompleted
O círculo permanece na hierarquia durante toda a sessão. Uma soltura aceita incrementa a contagem do destino; uma soltura não aceita não altera o conteúdo. Isso torna o exemplo útil para diagnosticar o feedback independentemente da reordenação ou da semântica de movimentação. O código foi inspecionado em relação às declarações obtidas; não foi compilado nem executado porque o ambiente disponível é Linux e não tem SwiftUI.
Como a alternativa do AppKit deve preservar a responsabilidade pelos eventos?
Para uma versão anterior do macOS compatível, substitua a implementação da origem do arraste por uma NSView hospedada por meio de NSViewRepresentable. O representable cria e atualiza a view nativa e conecta seus callbacks ao estado do SwiftUI. O SwiftUI controla o layout da view representada, portanto use modificadores de dimensionamento do SwiftUI em vez de atribuir você mesmo o frame da view representada. NSViewRepresentable
Implemente a alternativa como uma sequência controlada pela origem:
- No
mouseDown(with:)da view nativa de origem, retenha o evento real de pressionamento do mouse entregue a essa view. - Em
mouseDragged(with:), aplique o limite de arraste escolhido e impeça o início de uma segunda sessão. - Crie um
NSDraggingItema partir de um gravador de área de transferência em memória que carregue o identificador do item sintético. - Forneça uma imagem gerada da forma e defina seu frame de arraste no sistema de coordenadas da view de origem.
- Chame
beginDraggingSession(with:event:source:)usando o evento retido de pressionamento do mouse e o objeto que implementaNSDraggingSource. - Mostre o feedback da origem em
draggingSession(_:willBeginAt:)e restaure-o incondicionalmente emdraggingSession(_:endedAt:operation:). - Limpe o evento de entrada retido e os dados da sessão quando o rastreamento terminar. Mantenha a origem e o responsável pelo callback ativos até a conclusão da sessão.
Esses detalhes seguem o contrato explícito do AppKit: o método de início da sessão recebe o evento de pressionamento do mouse que a iniciou, os frames dos itens de arraste usam o sistema de coordenadas da view e a origem recebe mensagens do ciclo de vida. NSDraggingItem aceita um objeto que esteja em conformidade com NSPasteboardWriting; seu método conveniente de imagem única define o frame e o conteúdo do arraste. beginDraggingSession(with:event:source:), init(pasteboardWriter:), setDraggingFrame(_:contents:)
Para essa interação dentro do próprio app, retorne .copy para NSDraggingContext.withinApplication e um conjunto vazio de operações para outros contextos. Essa é uma escolha explícita de oferecer apenas cópias locais. O AppKit pergunta à origem quais operações ela permite por meio de draggingSession(_:sourceOperationMaskFor:). draggingSession(_:sourceOperationMaskFor:)
Restaure a aparência para todos os resultados de conclusão, e não apenas para uma cópia bem-sucedida. Mantenha quaisquer alterações no modelo condicionadas à operação que seu app realmente implementou. O ponto do callback de conclusão usa coordenadas da tela; não o compare diretamente com os limites locais de um destino do SwiftUI. draggingSession(_:endedAt:operation:)
Uma view nativa transparente colocada atrás de um .draggable existente do SwiftUI não se torna a origem dessa sessão apenas por estar em conformidade com NSDraggingSource. A origem é o objeto passado quando a sessão começa. Escolha um único responsável pelo início e pela conclusão e restrinja a superfície nativa de arraste à área de interação pretendida da forma.
Por que não usar o monitoramento do botão do mouse ou a desalocação do provider?
Um monitor de botão do mouse em todo o app observa o despacho de entrada, que é um limite diferente da conclusão do arraste. A Apple documenta que monitores locais de eventos não recebem eventos consumidos por loops de rastreamento aninhados. Essa limitação torna o monitor um substituto inadequado para o callback documentado de conclusão da origem. addLocalMonitorForEvents(matching:handler:)
Da mesma forma, o método deinit de um provider de itens informa sobre o ciclo de vida do objeto. O Swift chama um desalocador imediatamente antes de uma instância ser desalocada; esse contrato não determina quando um arraste terminou. Usar a destruição do provider como sinal de conclusão vincula o estado da interface à propriedade de referências, e não ao ciclo de vida do arraste. Deinitialization
Adicionar um segundo DragGesture junto com um arraste do sistema tem o mesmo problema conceitual: seu estado pertence àquele gesto. Use @GestureState quando o gesto local controlar a interação; use o callback da sessão da origem do arraste quando ela for controlada pelo arraste do sistema. GestureState
Verifique a correção com uma matriz de conclusão
Use o círculo sintético para verificar cada caminho separadamente. Estes são comportamentos esperados para a implementação, não resultados de testes relatados.
| Interação | Feedback esperado da origem | Resultado esperado no destino |
|---|---|---|
| Soltar o círculo dentro da área receptora | Volta ao normal | A contagem aumenta uma vez |
| Soltar sobre um espaço vazio da janela | Volta ao normal | A contagem permanece inalterada |
| Sair da área receptora enquanto ainda arrasta | Continua esmaecido | O destaque do destino é removido |
| Reentrar e concluir uma soltura válida | Volta ao normal ao concluir | A contagem aumenta uma vez |
| Soltar fora da janela do app sem um destino que aceite | Volta ao normal | A contagem permanece inalterada |
| Cancelar um arraste ativo | Volta ao normal | A contagem permanece inalterada |
| Repetir vários arrastes malsucedidos | Cada sessão restaura a aparência normalmente | Nenhuma cópia aceita se acumula |
Se a origem continuar esmaecida, primeiro verifique se o observador de conclusão envolve a origem real do arraste e se a origem continua presente. Se apenas o destaque do destino ficar preso, examine o tratamento de entrada e saída do destino e a limpeza compartilhada. Se a contagem de itens aceitos mudar após uma soltura rejeitada, examine a validação e a alteração do modelo separadamente da restauração visual.
A correção duradoura é deixar explícita a responsabilidade pela conclusão: a origem restaura o feedback de toda a sessão, o destino trata seu próprio estado de foco e o conteúdo aceito, e a limpeza da transferência segue seu ciclo de vida documentado. Com essas responsabilidades separadas, soltar fora de todos os destinos não deixa nenhum estado visual aguardando um callback do destino.
Fontes
onDragSessionUpdated(_:) — Apple
DragSession.Phase — Apple
DragSession.Phase.ended(_:) — Apple
draggable(_:) — Apple
DropDelegate — Apple
GestureState — Apple
NSViewRepresentable — Apple
init(pasteboardWriter:) — Apple
setDraggingFrame(_:contents:) — Apple
Deinitialization — Swift.org