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:

  1. No mouseDown(with:) da view nativa de origem, retenha o evento real de pressionamento do mouse entregue a essa view.
  2. Em mouseDragged(with:), aplique o limite de arraste escolhido e impeça o início de uma segunda sessão.
  3. Crie um NSDraggingItem a partir de um gravador de área de transferência em memória que carregue o identificador do item sintético.
  4. Forneça uma imagem gerada da forma e defina seu frame de arraste no sistema de coordenadas da view de origem.
  5. Chame beginDraggingSession(with:event:source:) usando o evento retido de pressionamento do mouse e o objeto que implementa NSDraggingSource.
  6. Mostre o feedback da origem em draggingSession(_:willBeginAt:) e restaure-o incondicionalmente em draggingSession(_:endedAt:operation:).
  7. 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