Para arrastrar y soltar dentro de tu app para Mac, el origen declara las operaciones admitidas, el destino elige una y tu modelo implementa el resultado. En SwiftUI en macOS 26 o posterior, configura ambos extremos y elimina el original solo después de que se complete un movimiento.
Este artículo crea una pequeña interacción de escritorio: arrastra un círculo sintético a «Copy here» o «Move here». Todo permanece en memoria. El ejemplo hace explícitos la operación, la identidad del elemento y la limpieza del origen, y luego asigna las mismas responsabilidades a AppKit.
¿Qué deben acordar el origen y el destino?
Una carga transferible describe qué se mueve entre las vistas. Copiar o mover describe qué ocurre con el original. Son decisiones distintas. La documentación de SwiftUI de Apple indica que, de forma predeterminada, el arrastre admite la copia; habilitar el movimiento añade otra operación admitida. Luego, el destino elige cómo recibir el elemento. DragConfiguration
Usa tres responsabilidades para diseñar la interacción:
| Responsabilidad | Copiar | Mover |
|---|---|---|
| Capacidad del origen | Ofrecer copiar | Ofrecer mover |
| Confirmación del destino | Insertar un elemento independiente | Recibir el elemento existente |
| Finalización en el origen | Conservar el original | Eliminar el original después de que se complete el movimiento |
En este ejemplo del círculo, una copia independiente recibe un identificador nuevo. Un movimiento conserva el identificador y cambia su ubicación. Es una decisión de diseño de la aplicación, coherente con el requisito de Swift de que una entidad identificable tenga una identidad estable; Swift deja en manos de la aplicación definir el alcance y la duración de esa identidad. Identifiable
La consecuencia práctica es que devolver una operación de movimiento no reubica un elemento en tus arreglos. Aun así, debes insertarlo en el destino y eliminarlo del origen. Apple muestra específicamente cómo observar la operación de arrastre completada para indicar al modelo cuándo eliminar un elemento del origen. onDragSessionUpdated(_:)
¿Qué API de SwiftUI debería usar una app para Mac?
Para esta implementación, usa un destino de implementación de macOS 26 o posterior y un SDK que incluya los modificadores de configuración documentados. Añade draggable(_:) al origen, dragConfiguration(_:) para declarar sus operaciones y dropConfiguration(_:) para seleccionar la operación del destino. Ambos modificadores de configuración están documentados como disponibles en macOS 26. dragConfiguration(_:), dropConfiguration(_:)
La sobrecarga actual de dropDestination(for:isEnabled:action:) proporciona un DropSession a su acción. La acción devuelve Void. Esto difiere de la sobrecarga anterior, cuya acción recibe una ubicación y devuelve un valor booleano. En el ejemplo, especifica isEnabled: de forma explícita para que quede clara la sobrecarga prevista. dropDestination(for:isEnabled:action:)
En el destino, inspecciona suggestedOperations antes de seleccionar una operación. Para un destino general que prioriza el movimiento, elige mover si está disponible; de lo contrario, elige copiar si está disponible; y, si no, elige prohibido. El ejemplo documentado de Apple utiliza esta alternativa de mover a copiar y señala que el cierre de configuración se ejecuta con frecuencia, así que debe ser ligero. dropConfiguration(_:)
Nuestros dos destinos tienen significados deliberadamente fijos. Copy here acepta copiar; Move here acepta mover. Un destino devuelve «prohibido» cuando la operación indicada no está disponible. Así, el resultado es predecible sin necesidad de una tecla modificadora.
Crea un ejemplo completo de copia y movimiento en memoria
Crea una app de SwiftUI para macOS, establece su destino de implementación en macOS 26 o posterior y reemplaza las declaraciones generadas de la app y de la vista de contenido por el siguiente archivo único. El ejemplo transfiere una cadena integrada que contiene el identificador del elemento, así que no necesita un tipo transferible personalizado ni declarar un tipo propio. Apple documenta tanto las cargas transferibles para los orígenes de arrastre como las cargas de cadena para los destinos de soltar. draggable(_:), dropDestination(for:isEnabled:action:)
Esta es una app mínima completa, pensada para una ventana abierta y un elemento de origen. Revisé sus referencias de API con la documentación consultada; no pude compilarla ni ejecutarla aquí porque Swift y el SDK de macOS no están disponibles.
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)
}
}
}
La configuración del origen ofrece las operaciones de copiar y mover dentro de la app. Desactiva las operaciones fuera de la app mediante el inicializador de macOS para operaciones externas. El destino también comprueba localSession, que, según la documentación de Apple, no está presente cuando el arrastre se inicia en otro lugar. Estos ajustes mantienen el ejemplo centrado en el elemento en memoria de su propia ventana. DragConfiguration.OperationsWithinApp, init(allowCopy:allowMove:allowDelete:), DropSession
La acción de soltar valida el token exacto antes de insertar nada. Copiar añade un identificador nuevo. Mover añade el identificador original y registra que este destino lo recibió. Luego, el origen elimina el original que muestra solo si la sesión termina con una operación de movimiento y existe ese registro de recepción. Este registro es una salvaguarda de la aplicación para esta demostración limitada, no una función de transacción de SwiftUI.
La declaración actual de las fases de arrastre de Apple incluye initial, active, ending, ended y dataTransferCompleted. El ejemplo usa el caso documentado ended(DropOperation) para el resultado de la operación. No elimina el origen mientras se arrastra sobre el destino ni durante la fase anterior de finalización. DragSession.Phase
Mantén explícitos los límites de propiedad y de los gestos
La cadena es un token de búsqueda, no el modelo completo del círculo. El destino lo busca en el estado del origen que administra esta vista. Para varios documentos o ventanas, reemplaza esa búsqueda local por un propietario explícito del modelo y un registro de recepción asociado a la sesión de arrastre y a los elementos concretos. Que un arrastre se origine dentro de la app no determina, por sí solo, qué documento es propietario del elemento.
Para arrastrar varios elementos, usa identificadores estables del modelo y obtén los identificadores arrastrados de la sesión cuando estén disponibles. Apple proporciona draggedItemIDs(for:) para cargas identificables o identificadores suministrados por separado. Evita eliminar lo que esté seleccionado cuando llegue la finalización; la selección y la identidad del elemento arrastrado son datos distintos. DragSession
Deja que el modificador draggable(_:) de SwiftUI instale la interacción de arrastre. No elimines elementos del modelo al construir la carga. Apple documenta que el modificador añade los gestos adecuados y advierte que no se debe enviar un cierre de exportación al actor principal. draggable(_:)
¿Cómo funciona el mismo acuerdo en AppKit?
Usa AppKit cuando tu destino de implementación o la arquitectura de vistas existente requiera su ciclo de vida explícito de arrastre. Su protocolo de origen declara las operaciones permitidas mediante draggingSession(_:sourceOperationMaskFor:). El contexto permite distinguir los destinos dentro y fuera de la aplicación. Para la interacción anterior, ofrece copiar y mover dentro de la app y ninguna operación fuera de ella. draggingSession(_:sourceOperationMaskFor:)
El destino se registra para el tipo de portapapeles correspondiente, luego elige una operación en draggingEntered(_:) y actualiza esa elección según sea necesario en draggingUpdated(_:). Apple exige que el destino tenga en cuenta la intersección entre las operaciones del origen y las operaciones y los tipos de datos que admite. Devuelve una sola operación, o ninguna si no hay una intersección aceptable. NSDraggingDestination, draggingEntered(_:)
Realiza la inserción en performDragOperation(_:), una vez que la preparación se haya completado correctamente. Devuelve true solo si el destino acepta los datos. El origen recibe la operación completada mediante draggingSession(_:endedAt:operation:); usa ese resultado, junto con las reglas de propiedad de tu modelo, para completar la eliminación en el origen cuando se trate de un movimiento. performDragOperation(_:), draggingSession(_:endedAt:operation:)
Si integras una superficie de arrastre de AppKit en una interfaz de SwiftUI, deja que esa superficie se encargue de la interacción de AppKit. beginDraggingSession(with:event:source:) recibe el evento de pulsación del ratón que inicia el arrastre, y los marcos de los elementos arrastrados usan el sistema de coordenadas de la vista. Conserva esa relación entre el evento y las coordenadas en lugar de inventar un evento a partir de un valor posterior del gesto de SwiftUI. beginDraggingSession(with:event:source:)
El código de destino de SwiftUI anterior puede devolver DropProposal(operation: .move) desde DropDelegate.dropUpdated(info:). Eso expresa la propuesta del destino. No sustituye la configuración de capacidades del origen, la observación de la operación completada ni las modificaciones de tu modelo. Si las API nuevas de origen de SwiftUI no están disponibles y necesitas control explícito de ambos extremos, el ciclo de vida de origen y destino de AppKit ofrece esa alternativa. dropUpdated(info:), NSDraggingSource
Verifica la operación y el modelo a la vez
Para el ejemplo, usa esta lista de estados esperados. Son condiciones de aceptación derivadas de la implementación, no resultados de pruebas informados:
| Acción | Original | Destino de copia | Destino de movimiento |
|---|---|---|---|
Soltar una vez en Copy here |
Permanece | Recibe un identificador nuevo | Sin cambios |
| Copiar otra vez | Permanece | Recibe otro identificador nuevo | Sin cambios |
Soltar en Move here |
Desaparece tras completarse el movimiento | Sin cambios | Recibe el identificador original |
| Cancelar o soltar lejos de ambos destinos | Permanece | Sin cambios | Sin cambios |
Si el comportamiento no coincide, inspecciona las etapas en orden: capacidades del origen, operación del destino, inserción en el destino y finalización en el origen. Un original duplicado después del movimiento apunta a que falta la limpieza del origen. Dos eliminaciones apuntan a propietarios del modelo que compiten entre sí. Una copia inesperada apunta a que el origen no ofreció mover o a que el destino eligió copiar.
Deja clara la responsabilidad final: la operación negociada describe la transferencia, mientras que tu modelo implementa la ubicación y la identidad del elemento. Ofrece las operaciones que admites, elige una operación permitida en el destino y realiza cada cambio del modelo una sola vez en el límite de ciclo de vida documentado.
Fuentes
DragConfiguration — Apple
Identifiable — Apple
onDragSessionUpdated(_:) — Apple
dragConfiguration(_:) — Apple
dropConfiguration(_:) — Apple
draggable(_:) — Apple
DropSession — Apple
DragSession.Phase — Apple
DragSession — Apple
NSDraggingDestination — Apple
draggingEntered(_:) — Apple
performDragOperation(_:) — Apple
dropUpdated(info:) — Apple
NSDraggingSource — Apple