Para arrastar e soltar dentro do seu app para Mac, a origem declara as operações compatíveis, o destino escolhe uma e seu modelo implementa o resultado. No SwiftUI no macOS 26 ou posterior, configure as duas extremidades e remova o original somente depois que uma movimentação for concluída.

Este artigo apresenta uma pequena interação para desktop: arraste um círculo sintético para Copy here ou Move here. Tudo permanece na memória. O exemplo deixa explícitos a operação, a identidade do item e a limpeza da origem e, em seguida, associa as mesmas responsabilidades ao AppKit.

Com o que a origem e o destino precisam concordar?

Uma carga útil transferível descreve o que viaja entre as visualizações. Copiar ou mover descreve o que acontece com o original. São decisões distintas. A documentação do SwiftUI da Apple diz que o arraste oferece suporte à cópia por padrão; habilitar a movimentação adiciona outra operação compatível. Em seguida, o destino escolhe como receber o item. DragConfiguration

Use três responsabilidades para projetar a interação:

Responsabilidade Cópia Movimentação
Capacidade da origem Oferecer cópia Oferecer movimentação
Confirmação do destino Inserir um item independente Receber o item existente
Conclusão na origem Manter o original Remover o original após a movimentação concluída

Neste exemplo do círculo, uma cópia independente recebe um novo identificador. Uma movimentação preserva o identificador e altera a localização. Essa é uma escolha de projeto do app, compatível com o requisito do Swift de que uma entidade identificável tenha identidade estável; o Swift deixa o escopo e a duração dessa identidade a cargo do app. Identifiable

A consequência prática é que retornar uma operação de movimentação não realoca um elemento nos seus arrays. Você ainda precisa inserir o item no destino e removê-lo da origem. A Apple demonstra especificamente como observar a operação concluída de arraste para informar ao modelo quando deve remover um item da origem. onDragSessionUpdated(_:)

Quais APIs do SwiftUI um app para Mac deve usar?

Para esta implementação, use um destino de implantação no macOS 26 ou posterior e um SDK que contenha os modificadores de configuração documentados. Adicione draggable(_:) à origem, dragConfiguration(_:) para declarar suas operações e dropConfiguration(_:) para selecionar a operação do destino. Ambos os modificadores de configuração estão documentados como disponíveis no macOS 26. dragConfiguration(_:), dropConfiguration(_:)

A sobrecarga atual de dropDestination(for:isEnabled:action:) fornece um DropSession à sua ação. A ação retorna Void. Isso difere da sobrecarga mais antiga, cuja ação recebe uma localização e retorna um valor Boolean. Especifique isEnabled: explicitamente no exemplo para deixar clara a sobrecarga pretendida. dropDestination(for:isEnabled:action:)

No destino, inspecione suggestedOperations antes de selecionar uma operação. Para um destino genérico que prioriza a movimentação, escolha movimentação quando oferecida; caso contrário, escolha cópia quando oferecida; se nenhuma estiver disponível, escolha proibido. O exemplo documentado pela Apple usa essa alternativa de movimentação para cópia e observa que a closure de configuração é executada com frequência, portanto mantenha-a simples. dropConfiguration(_:)

Nossos dois destinos têm significados fixos de propósito. Copy here aceita cópia; Move here aceita movimentação. Um destino retorna proibido quando a operação indicada no nome não está disponível. Isso torna o resultado previsível sem exigir um modificador de teclado.

Crie um exemplo completo de cópia e movimentação na memória

Crie um app SwiftUI para macOS, defina o destino de implantação como macOS 26 ou posterior e substitua as declarações geradas do app e da visualização de conteúdo pelo arquivo único a seguir. O exemplo transfere uma string incorporada que contém o identificador de um item, portanto não precisa de um tipo transferível personalizado nem de uma declaração de tipo personalizada. A Apple documenta tanto cargas úteis transferíveis para origens de arraste quanto cargas úteis de string para destinos de soltura. draggable(_:), dropDestination(for:isEnabled:action:)

Este é um app mínimo completo, destinado a uma janela aberta e a um único item de origem. Conferi as referências da API com a documentação consultada; não foi possível compilá-lo nem executá-lo aqui porque o Swift e o SDK do macOS não estão disponíveis.

import SwiftUI
import Foundation

@main
struct CopyMoveDemoApp: App {
    var body: some Scene {
        WindowGroup {
            CopyMoveDemo()
        }
    }
}

struct CopyMoveDemo: View {
    @State private var sourceID = UUID()
    @State private var sourcePresent = true
    @State private var copiedIDs: [UUID] = []
    @State private var movedIDs: [UUID] = []
    @State private var moveReceived = false

    private var sourceConfiguration: DragConfiguration {
        DragConfiguration(
            operationsWithinApp: .init(allowMove: true),
            operationsOutsideApp: .init(
                allowCopy: false,
                allowMove: false,
                allowDelete: false
            )
        )
    }

    var body: some View {
        VStack(spacing: 24) {
            Text("Drag the circle to a target")
                .font(.headline)

            if sourcePresent {
                Circle()
                    .fill(.blue)
                    .frame(width: 90, height: 90)
                    .overlay(Text("Circle").foregroundStyle(.white))
                    .draggable(sourceID.uuidString)
                    .dragConfiguration(sourceConfiguration)
                    .onDragSessionUpdated { session in
                        if session.phase == .initial {
                            moveReceived = false
                        }
                        if session.phase == .ended(.move), moveReceived {
                            sourcePresent = false
                        }
                        if case .ended = session.phase {
                            moveReceived = false
                        }
                    }
            } else {
                Text("Original moved")
                    .frame(width: 90, height: 90)
            }

            HStack(alignment: .top, spacing: 20) {
                target(moving: false)
                target(moving: true)
            }
        }
        .padding(24)
        .frame(minWidth: 520, minHeight: 320)
    }

    private func target(moving: Bool) -> some View {
        let operation: DropOperation = moving ? .move : .copy
        let ids = moving ? movedIDs : copiedIDs

        return VStack(spacing: 10) {
            Text(moving ? "Move here" : "Copy here")
                .font(.headline)
            ForEach(ids, id: \.self) { id in
                Text("Circle \(id.uuidString.prefix(4))")
            }
            Spacer(minLength: 20)
        }
        .frame(width: 210, height: 150)
        .background(.blue.opacity(0.08))
        .contentShape(Rectangle())
        .dropDestination(for: String.self, isEnabled: true) {
            tokens, session in
            guard session.localSession != nil,
                  sourcePresent,
                  tokens == [sourceID.uuidString] else {
                return
            }
            if moving {
                guard !movedIDs.contains(sourceID) else { return }
                movedIDs.append(sourceID)
                moveReceived = true
            } else {
                copiedIDs.append(UUID())
            }
        }
        .dropConfiguration { session in
            guard sourcePresent,
                  session.localSession != nil,
                  session.itemsCount == 1,
                  session.suggestedOperations.contains(operation) else {
                return DropConfiguration(operation: .forbidden)
            }
            return DropConfiguration(operation: operation)
        }
    }
}

A configuração da origem oferece cópia e movimentação dentro do app. Ela desativa as operações fora do app usando o inicializador do macOS para operações externas ao app. O destino também verifica localSession, que, segundo a documentação da Apple, não está presente quando o arraste tem origem em outro lugar. Essas configurações mantêm o exemplo focado no item em memória da própria janela. DragConfiguration.OperationsWithinApp, init(allowCopy:allowMove:allowDelete:), DropSession

A ação de soltura valida o token exato antes de inserir qualquer coisa. A cópia adiciona um novo identificador. A movimentação adiciona o identificador original e registra que esse destino o recebeu. Em seguida, a origem remove o original exibido somente quando a sessão termina com uma movimentação e há registro desse recebimento. Esse registro é uma proteção no nível do app para esta demonstração limitada, não um recurso de transação do SwiftUI.

A declaração atual das fases de arraste da Apple inclui initial, active, ending, ended e dataTransferCompleted. O exemplo usa o caso documentado ended(DropOperation) para o resultado da operação. Ele não remove a origem enquanto o item está sobre o destino nem durante a fase anterior de encerramento. DragSession.Phase

Deixe explícitos os limites de propriedade e dos gestos

A string é um token de consulta, não o modelo completo do círculo. O destino consulta o estado da origem pertencente a esta visualização. Para vários documentos ou janelas, substitua essa consulta local por um proprietário explícito do modelo e por um registro associado à sessão de arraste e aos itens reais. Um arraste iniciado dentro do app, por si só, não estabelece qual documento é proprietário de um item.

Para vários itens arrastáveis, use identificadores estáveis do modelo e obtenha da sessão os identificadores dos itens arrastados, quando disponíveis. A Apple fornece draggedItemIDs(for:) para cargas úteis identificáveis ou identificadores fornecidos separadamente. Evite remover o que estiver selecionado no momento em que a conclusão chegar; a seleção e a identidade do item arrastado são dados distintos. DragSession

Deixe que o modificador draggable(_:) do SwiftUI instale a interação de arraste. Não faça a construção da carga útil remover itens do modelo. A Apple documenta que o modificador adiciona os gestos apropriados e alerta contra o envio de uma closure de exportação para o ator principal. draggable(_:)

Como o mesmo acordo funciona no AppKit?

Use o AppKit quando o destino de implantação ou a arquitetura de visualizações existente exigir seu ciclo de vida explícito de arraste. O protocolo da origem declara as operações permitidas por meio de draggingSession(_:sourceOperationMaskFor:). O contexto permite distinguir destinos dentro e fora do app. Para a interação acima, ofereça cópia e movimentação dentro do app e nenhuma operação fora dele. draggingSession(_:sourceOperationMaskFor:)

O destino se registra para o tipo de área de transferência pertinente, escolhe uma operação em draggingEntered(_:) e atualiza essa escolha conforme necessário em draggingUpdated(_:). A Apple exige que o destino considere a sobreposição entre as operações da origem e as operações e os tipos de dados compatíveis com ele. Retorne uma operação ou nenhuma quando não houver sobreposição aceitável. NSDraggingDestination, draggingEntered(_:)

Faça a inserção em performDragOperation(_:), depois que a preparação for concluída com sucesso. Retorne true somente quando o destino aceitar os dados. A origem recebe a operação concluída por meio de draggingSession(_:endedAt:operation:); use esse resultado, junto com as regras de propriedade do modelo, para concluir a remoção do lado da origem em uma movimentação. performDragOperation(_:), draggingSession(_:endedAt:operation:)

Se incorporar uma área de arraste do AppKit em uma interface SwiftUI, deixe essa área controlar a interação do AppKit. beginDraggingSession(with:event:source:) recebe o evento de pressionamento do mouse que inicia o arraste, e os quadros dos itens arrastados usam o sistema de coordenadas da visualização. Preserve essa relação entre evento e coordenadas em vez de fabricar um evento a partir de um valor posterior de gesto do SwiftUI. beginDraggingSession(with:event:source:)

O código mais antigo de destino do SwiftUI pode retornar DropProposal(operation: .move) de DropDelegate.dropUpdated(info:). Isso expressa a proposta do destino. Não substitui a configuração dos recursos da origem, a observação da operação concluída nem as mutações do seu modelo. Quando as APIs mais novas de origem do SwiftUI não estiverem disponíveis e você precisar de controle explícito das duas extremidades, o ciclo de vida de origem e destino do AppKit oferece esse caminho. dropUpdated(info:), NSDraggingSource

Verifique a operação e o modelo em conjunto

Para o exemplo, use esta lista de estados esperados. Estas são condições de aceitação derivadas da implementação, não resultados de testes relatados:

Ação Original Destino de cópia Destino de movimentação
Soltar uma vez em Copy here Permanece Recebe um novo identificador Sem alterações
Copiar novamente Permanece Recebe outro novo identificador Sem alterações
Soltar em Move here Desaparece após a movimentação concluída Sem alterações Recebe o identificador original
Cancelar ou soltar longe dos dois destinos Permanece Sem alterações Sem alterações

Quando o comportamento divergir, inspecione as etapas nesta ordem: recursos da origem, operação do destino, inserção no destino e conclusão na origem. Um original duplicado após a movimentação indica que a limpeza da origem pode estar faltando. Duas remoções indicam que há proprietários concorrentes do modelo. Uma cópia inesperada indica que a origem não ofereceu movimentação ou que o destino selecionou cópia.

Mantenha clara a responsabilidade final: a operação negociada descreve a transferência, enquanto seu modelo implementa a localização e a identidade do item. Ofereça as operações compatíveis, escolha uma operação permitida no destino e faça cada alteração do modelo uma única vez, no limite do ciclo de vida documentado.

Fontes