En un lienzo de Mac con zonas de destino superpuestas, coloca la gestión nativa del arrastre en un único ancestro estable y determina el destino lógico a partir del estado actual del modelo. Vuelve a comprobar la elegibilidad al confirmar el destino; un callback que rechaza el elemento no implica, según la documentación, que se transfiera a otra vista.

Este artículo implementa esa interacción en una app que contiene etiquetas sintéticas de formas y dos paneles superpuestos. La tarea concreta consiste en permitir que un panel inferior reciba un elemento arrastrado cuando el panel superior deja de aceptarlos, incluso si su elegibilidad cambia durante el arrastre. La política de enrutamiento que se presenta a continuación es un diseño de aplicación derivado de los callbacks documentados.

¿Por qué cambiar el estado no transfiere el arrastre de forma fiable a otro destino?

Empieza por separar tres preguntas: qué vista nativa recibe el arrastre, qué panel lógico considera elegible tu app y qué modelo recibe el elemento al final. Un cambio de estado puede cambiar la respuesta a la segunda pregunta sin establecer una nueva respuesta para la primera.

Apple documenta validateDrop(info:) de SwiftUI como una validación que ocurre cuando un arrastre compatible entra en un destino. Documenta dropUpdated(info:) como una actualización que ocurre cuando un arrastre validado se mueve dentro de la vista modificada. Ninguna de las dos descripciones promete una nueva búsqueda de destino cada vez que cambia un estado ajeno a la vista propietaria. Validación de SwiftUI y actualizaciones del destino de arrastre.

El límite de AppKit es más explícito: solo un destino recibe una secuencia de mensajes draggingUpdated(_:) a la vez. Cuando dos vistas superpuestas son destinos válidos, la que está más arriba recibe esos mensajes hasta que se suelta el elemento arrastrado o se arrastra fuera de ella. Esto describe el enrutamiento de AppKit; no establece un contrato general de prioridad entre vistas hermanas en SwiftUI. Actualizaciones de destinos en AppKit.

Devolver false desde la acción de arrastre antigua de SwiftUI indica que el elemento no se aceptó. Apple no describe ese valor de retorno como una solicitud para volver a intentarlo con un destino situado debajo. Del mismo modo, devolver nil desde dropUpdated(info:) conserva la última propuesta válida o, de forma predeterminada, copia el elemento; no significa «dejar pasar». Gestión de arrastre basada en closures y comportamiento de las propuestas.

La consecuencia práctica es que la aplicación debe definir explícitamente la alternativa. En un único lienzo, un receptor nativo puede elegir entre varios paneles lógicos sin depender de que el sistema transfiera el arrastre entre receptores secundarios superpuestos.

Define la propiedad y la alternativa antes de añadir modificadores

En este ejemplo, el panel naranja está detrás del azul. El naranja siempre acepta las etiquetas de muestra. El azul las acepta solo mientras blueEnabled sea verdadero. El comportamiento previsto es el siguiente:

Ubicación del puntero El azul acepta elementos arrastrados Destinatario lógico
Zona solo naranja Cualquier valor Naranja
Superposición Sí Azul
Superposición No Naranja
Zona solo azul No Ninguno
Fuera de ambos paneles Cualquier valor Ninguno

Esta tabla sirve de guía para tomar decisiones durante la implementación. En particular, desactivar el azul no convierte todos los puntos de su rectángulo en destinos naranjas. El naranja recibe el elemento solo si el puntero también está dentro de su rectángulo.

Mantén el estado que determina el enrutamiento por encima de los paneles visuales. Apple recomienda colocar un valor de estado en la vista más alta que lo necesite y compartirlo con sus descendientes. En este caso, un padre es propietario de un modelo que contiene la elegibilidad, la geometría y los elementos recibidos. Los rectángulos secundarios definen la apariencia, no gestionan destinos de arrastre independientes. Propiedad del estado en SwiftUI.

Lee ese modelo cuando se ejecute la acción de destino. Evita guardar un valor Booleano de elegibilidad o un identificador de destino al entrar y tratarlo después como si fuera autoritativo. Las closures de Swift pueden conservar valores y referencias del contexto en que se crearon; una referencia conservada también puede seguir apuntando a un modelo anterior tras reemplazar al propietario. Comprueba qué objeto captura realmente cada callback. Captura de valores en closures de Swift.

Para reemplazar al propietario, elige una política aparte: resolver con el nuevo propietario o rechazar el trabajo que corresponda al anterior. Un diseño de rechazo útil asigna un número de revisión al propietario, lo incrementa al reemplazarlo y compara la revisión capturada antes de confirmar el trabajo diferido. Un valor transitorio de «resaltado actual» no debería determinar quién es el propietario.

Crea una superficie estable para recibir arrastres en SwiftUI

El siguiente ejemplo completo requiere una app de SwiftUI para macOS con un destino de implementación para macOS 26 y un SDK que incluya dropDestination(for:isEnabled:action:). Apple documenta esa sobrecarga como una API que acepta un tipo Transferable, un indicador de habilitación y una acción que recibe los elementos decodificados junto con una DropSession. El destino ocupa el tamaño y la posición de la vista modificada. API actual para destinos de arrastre.

Crea un proyecto nuevo de app para macOS con SwiftUI. Sustituye la declaración de app generada por los dos bloques de código siguientes, colocados consecutivamente en un mismo archivo Swift. Juntos forman un programa. Solo usa cadenas y rectángulos en memoria. Las etiquetas de origen siguen disponibles después de soltarlas, por lo que el ejemplo funciona como una operación de copia.

Modelo y enrutamiento determinista

El modelo comprueba primero el azul para expresar la prioridad visual elegida. Tanto el renderizado como el enrutamiento usan los mismos rectángulos. Su método de confirmación vuelve a calcular el destinatario con la elegibilidad actual y valida todo el lote de muestra antes de añadir nada.

import SwiftUI
import Combine

@MainActor
final class CanvasModel: ObservableObject {
    enum Zone: String {
        case orange = "Orange"
        case blue = "Blue"
    }

    let orangeRect = CGRect(x: 30, y: 30, width: 230, height: 170)
    let blueRect = CGRect(x: 150, y: 100, width: 230, height: 170)
    let samples = ["Circle", "Square"]

    @Published var blueEnabled = true
    @Published private(set) var orangeItems: [String] = []
    @Published private(set) var blueItems: [String] = []
    @Published private(set) var message = "Drag a sample onto a panel."

    func recipient(at point: CGPoint) -> Zone? {
        if blueEnabled && blueRect.contains(point) {
            return .blue
        }
        if orangeRect.contains(point) {
            return .orange
        }
        return nil
    }

    func receive(_ items: [String], at point: CGPoint, isLocal: Bool) {
        guard isLocal,
              !items.isEmpty,
              items.allSatisfy({ samples.contains($0) }) else {
            message = "No items added: use this window's sample labels."
            return
        }
        guard let zone = recipient(at: point) else {
            message = "No items added: this location has no eligible panel."
            return
        }
        switch zone {
        case .orange:
            orangeItems.append(contentsOf: items)
        case .blue:
            blueItems.append(contentsOf: items)
        }
        message = "Added \(items.joined(separator: ", ")) to \(zone.rawValue)."
    }
}

App y receptor de arrastres

El rectángulo padre contiene el único modificador de destino de arrastre. Ninguno de los paneles de color registra un destino en conflicto. DropSession proporciona una ubicación en el espacio de coordenadas local del destino y un localSession que identifica un arrastre iniciado en la app. El modelo también comprueba los valores de muestra. Información de la sesión de arrastre.

@main
struct DropRoutingDemoApp: App {
    var body: some Scene {
        WindowGroup {
            CanvasDemo()
        }
    }
}

@MainActor
struct CanvasDemo: View {
    @StateObject private var model = CanvasModel()
    @State private var scheduledChange: Task<Void, Never>?

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack {
                ForEach(model.samples, id: \.self) { item in
                    Text(item)
                        .padding(10)
                        .background(Color.gray.opacity(0.15))
                        .draggable(item)
                }
            }

            Toggle("Blue accepts drops", isOn: $model.blueEnabled)

            Button("Toggle blue in 3 seconds") {
                scheduledChange?.cancel()
                scheduledChange = Task { @MainActor in
                    do {
                        try await Task.sleep(for: .seconds(3))
                    } catch {
                        return
                    }
                    model.blueEnabled.toggle()
                }
            }

            ZStack(alignment: .topLeading) {
                Rectangle().fill(Color.gray.opacity(0.08))
                panel("Orange", rect: model.orangeRect,
                      color: .orange, opacity: 0.55)
                panel("Blue", rect: model.blueRect,
                      color: .blue,
                      opacity: model.blueEnabled ? 0.55 : 0.15)
            }
            .frame(width: 420, height: 300)
            .dropDestination(for: String.self, isEnabled: true) {
                items, session in
                model.receive(items, at: session.location,
                              isLocal: session.localSession != nil)
            }

            Text(model.message)
            Text("Orange: \(model.orangeItems.joined(separator: ", "))")
            Text("Blue: \(model.blueItems.joined(separator: ", "))")
        }
        .padding(20)
        .onDisappear {
            scheduledChange?.cancel()
        }
    }

    private func panel(_ title: String, rect: CGRect,
                       color: Color, opacity: Double) -> some View {
        Rectangle()
            .fill(color.opacity(opacity))
            .overlay(Text(title))
            .frame(width: rect.width, height: rect.height)
            .position(x: rect.midX, y: rect.midY)
    }
}

draggable(_:) de SwiftUI instala los gestos necesarios para iniciar el arrastre mediante la interacción del usuario. El botón con retraso solo cambia el estado de elegibilidad del ejemplo; no inicia, reinicia ni transfiere una sesión de arrastre. Comportamiento de los orígenes de arrastre en SwiftUI.

Pulsa Alternar el azul en 3 segundos y, a continuación, arrastra Circle hasta la zona superpuesta y mantenlo allí hasta que el azul se desvanezca. Suéltalo dentro de la superposición: el destinatario previsto es el naranja. Si lo sueltas en la zona exclusiva del azul mientras está desactivado, no cambia ninguna de las dos matrices de elementos y el estado explica el motivo.

Este receptor permanece habilitado en todo el lienzo para poder aplicar la política de enrutamiento completa. Por ello, el ejemplo no ofrece un cursor nativo de rechazo según la ubicación ni un resaltado al pasar el puntero. Su estado indica si la aplicación aceptó el elemento después de soltarlo. Si el producto necesita esa respuesta visual, añade por separado la gestión de propuestas y de eventos al pasar el puntero.

El código se revisó comparándolo con las declaraciones de API recuperadas. No se compiló ni se ejecutó aquí porque el entorno disponible no cuenta con una cadena de herramientas de Swift ni con un entorno de ejecución de macOS.

Gestiona por separado las actualizaciones al pasar el puntero y los destinos de implementación anteriores

Si admites sistemas que no incluyen la sobrecarga más reciente, conserva la misma política de modelo y geometría, pero elige una API de recepción disponible. La API anterior dropDestination(for:action:isTargeted:) pasa los elementos decodificados y un punto local; su resultado Booleano indica si la operación tuvo éxito. Su closure isTargeted describe la entrada y la salida del área nativa del destino, que en este diseño abarca todo el lienzo. No identifica el panel lógico situado bajo el puntero. Contrato anterior de destinos de arrastre.

Para validar explícitamente y proponer operaciones, onDrop(of:delegate:) instala un DropDelegate. Usa validateDrop(info:) para la elegibilidad inicial, dropUpdated(info:) para las propuestas actuales y performDrop(info:) para la validación final y la incorporación. Devolver .forbidden describe una actividad no permitida en esa ubicación; no documenta una transferencia a otro receptor nativo. Destinos basados en delegados y operaciones de arrastre.

Al usar proveedores, hay un límite importante respecto a cuándo pueden cargarse: DropInfo.itemProviders(for:) solo es válido durante performDrop. Inicia la carga del contenido del proveedor antes de que ese método devuelva el control. La finalización puede llegar más tarde, así que vuelve a comprobar la identidad y la revisión del destino, así como que siga siendo elegible, antes de cambiar el modelo. Informa del rechazo o del fallo de carga en vez de descartar el resultado sin avisar. Acceso a proveedores y requisitos para completar el arrastre.

Para mostrar información al pasar el puntero, guarda los datos transitorios de la sesión por separado de los elementos recibidos. La documentación más reciente de DropSession remite a onDropSessionUpdated(_:) para observar los cambios de fase. No des por hecho que un cambio de estado del modelo generará otra actualización del puntero; si ya tienes una ubicación válida durante el desplazamiento, vuelve a calcular el resaltado lógico también cuando cambie la elegibilidad. La aceptación final debe volver a llamar a la función de enrutamiento. Ciclo de vida de una sesión de arrastre.

¿Cuándo debería usar AppKit el lienzo?

Usa un destino de AppKit cuando la interacción requerida dependa de actualizaciones nativas periódicas documentadas mientras el puntero permanece inmóvil. wantsPeriodicDraggingUpdates() de AppKit controla este comportamiento: devolver false limita las actualizaciones al movimiento del ratón o a cambios de modificadores; de lo contrario, se producen actualizaciones periódicas aunque nada cambie. Esto permite volver a evaluar la operación actual durante un arrastre estacionario de una forma documentada. Actualizaciones periódicas durante el arrastre.

La arquitectura correspondiente sigue usando un único destino para el lienzo. Registra los tipos de pasteboard que acepta, resuelve los paneles lógicos durante draggingEntered(_:) y draggingUpdated(_:), comprueba la aceptación al soltar y añade el elemento en performDragOperation(_:). Apple describe específicamente cómo adaptar una operación a zonas gráficas o de texto dentro de una misma vista de destino. Ciclo de vida de un destino en AppKit y operaciones que dependen de la ubicación.

Al integrar ese destino con NSViewRepresentable, actualiza la referencia al modelo, la configuración de elegibilidad y los callbacks en updateNSView(_:context:). La creación inicial por sí sola no puede sincronizar cambios posteriores del propietario; Apple señala que el método de actualización es el lugar donde se pone la vista de AppKit al día con el estado de SwiftUI. Actualización de una vista de AppKit.

Conserva también el límite del evento de origen. beginDraggingSession(with:event:source:) de AppKit inicia una sesión a partir de los elementos que se arrastran y el evento inicial de pulsación del ratón. No es una API para cambiar el destino durante el arrastre. Mantén el origen de SwiftUI existente si satisface la tarea; usa un origen de AppKit solo si también necesitas controlar la interacción del lado del origen. Inicio de un arrastre en AppKit.

Verifica los cambios de estado en los puntos que afectan a quién recibe el elemento

Realiza las siguientes comprobaciones manuales en las versiones de macOS que admitas. Te ayudarán a distinguir el enrutamiento correcto de un cambio visual que solo parece funcionar:

  • Suelta el elemento en la superposición con el azul habilitado: solo la matriz del azul añade un elemento.
  • Desactiva el azul antes de arrastrar: la superposición dirige el elemento al naranja.
  • Desactiva el azul mientras el arrastre está detenido: la aceptación final tiene en cuenta ese estado desactivado.
  • Suelta el elemento en la zona exclusiva del azul mientras está desactivado: ninguna de las dos matrices cambia y el estado explica el resultado.
  • Vuelve a activar el azul durante el arrastre: la superposición dirige el elemento al azul en el momento de confirmar la operación.
  • Cancela el arrastre: ninguna de las dos matrices cambia.
  • Si tu app reemplaza a los propietarios del lienzo, reemplaza uno durante un arrastre y comprueba la política elegida para este caso.
  • Si tu implementación carga proveedores de forma asíncrona, cambia el propietario antes de que termine la carga y comprueba las verificaciones de revisión y que los fallos se comuniquen de forma visible.

En un lienzo desplazable, ampliable o redimensionable, amplía el mismo diseño convirtiendo los límites de cada panel y la ubicación del arrastre a un único espacio de coordenadas. Los rectángulos fijos de este ejemplo evitan esa conversión; copiar sus valores a un diseño transformado rompería la política de enrutamiento.

Un receptor nativo estable y una función de enrutamiento basada en el estado actual ofrecen a esta interacción un resultado explícito: el panel elegible superior recibe el elemento, un panel inferior elegible recibe el elemento como alternativa en la zona superpuesta y, en una ubicación sin cobertura, el modelo no cambia. Comparte esa política entre la respuesta visual al pasar el puntero y la aceptación final; esta última debe realizar su propia comprobación.

Fuentes