Mantén sin cambios la posición confirmada de la forma mientras SwiftUI muestra la vista previa del arrastre y, después, confirma la traslación final en onEnded. Así se conserva la posición inicial hasta que termina el gesto sin depender del momento en que AppKit registra la liberación del ratón. Aquí tienes un ejemplo completo para mover un rectángulo en tu propia app para Mac.

¿Qué límite de finalización debe ser el responsable del estado inicial?

Para una interacción que mueve una forma, la tarea concreta consiste en calcular cada vista previa y la posición final a partir de la misma posición inicial. El límite de finalización adecuado es la finalización correcta del gesto de esa forma. Apple documenta que onEnded(_:) proporciona el valor final del gesto y se ejecuta solo cuando el gesto termina correctamente. Recomienda @GestureState para el estado temporal que debe restablecerse independientemente de cómo termine el gesto. Documentación de onEnded(_:) de Apple.

Asigna a la interacción dos tipos de estado: una posición confirmada en el modelo y una traslación temporal de vista previa. Durante el arrastre, muestra la suma de ambas. Al finalizar correctamente, aplica una sola vez la traslación final a la posición confirmada. Este diseño se deriva de la distinción que hace Apple entre las actualizaciones transitorias de gestos y las actualizaciones permanentes del estado. Añadir interactividad con gestos.

Información Responsable en este ejemplo Duración
Centro confirmado del rectángulo Modelo privado @State Persiste al finalizar el gesto
Traslación actual de la vista previa @GestureState Se restablece cuando el gesto queda inactivo
Traslación final Argumento de la función de retorno onEnded Se usa sincrónicamente para confirmar el movimiento
Evento de liberación del ratón de AppKit Gestión de eventos de AppKit Indica que se soltó un botón; no define la finalización del gesto de SwiftUI

Apple documenta @State como almacenamiento gestionado por SwiftUI y fuente de verdad para los valores de una jerarquía de vistas. @GestureState se restablece automáticamente cuando su gesto queda inactivo. Mantener la posición de referencia en el primero y la vista previa en el segundo da a cada valor la duración que necesita esta interacción. State, GestureState.

Crea un ejemplo completo de arrastre de rectángulo para Mac

Crea en Xcode una app SwiftUI para macOS con un destino de implementación de macOS 14 o posterior. Reemplaza la declaración de app generada y la vista de contenido por el siguiente archivo único y asegúrate de que el destino tenga una sola declaración @main. El ejemplo usa un rectángulo sintético almacenado íntegramente en memoria. La documentación indica que sus API de espacio de coordenadas con nombre están disponibles a partir de macOS 14. Inicializador de gesto de arrastre, Espacios de coordenadas con nombre.

import SwiftUI

@main
struct GestureStartDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ShapeCanvas()
        }
    }
}

private struct SampleItem {
    var center = CGPoint(x: 160, y: 140)
}

private struct ShapeCanvas: View {
    @State private var item = SampleItem()
    @GestureState private var previewTranslation = CGSize.zero

    private let canvasWidth: CGFloat = 600
    private let canvasHeight: CGFloat = 360
    private let shapeWidth: CGFloat = 80
    private let shapeHeight: CGFloat = 60

    private func projectedCenter(_ translation: CGSize) -> CGPoint {
        let proposedX = item.center.x + translation.width
        let proposedY = item.center.y + translation.height
        let halfWidth = shapeWidth / 2
        let halfHeight = shapeHeight / 2

        return CGPoint(
            x: min(max(proposedX, halfWidth),
                   canvasWidth - halfWidth),
            y: min(max(proposedY, halfHeight),
                   canvasHeight - halfHeight)
        )
    }

    private var moveGesture: some Gesture {
        DragGesture(
            minimumDistance: 3,
            coordinateSpace: .named("shapeCanvas")
        )
        .updating($previewTranslation) { value, translation, _ in
            translation = value.translation
        }
        .onEnded { value in
            let finalCenter = projectedCenter(value.translation)
            item.center = finalCenter
        }
    }

    var body: some View {
        let displayedCenter = projectedCenter(previewTranslation)

        VStack(alignment: .leading, spacing: 12) {
            Text("Drag the blue rectangle")
                .font(.headline)

            ZStack {
                Color.gray.opacity(0.12)

                RoundedRectangle(cornerRadius: 10)
                    .fill(Color.blue)
                    .frame(width: shapeWidth, height: shapeHeight)
                    .contentShape(Rectangle())
                    .gesture(moveGesture)
                    .position(
                        x: displayedCenter.x,
                        y: displayedCenter.y
                    )
            }
            .frame(width: canvasWidth, height: canvasHeight)
            .coordinateSpace(.named("shapeCanvas"))
            .clipped()

            Text(
                "Committed center: " +
                "\(Int(item.center.x)), \(Int(item.center.y))"
            )
            .monospacedDigit()
        }
        .padding(20)
    }
}

La invariante clave es que item.center nunca cambia en updating. Sigue siendo la posición de referencia del modelo del gesto hasta que onEnded calcula finalCenter. El cálculo final lee directamente value.translation, por lo que no necesita que la propiedad de vista previa conserve su último valor durante la finalización.

El gesto mide el movimiento en el espacio de coordenadas con nombre del lienzo contenedor. La propiedad position(x:y:) del rectángulo coloca su centro en ese mismo espacio principal. Usar el mismo espacio evita mezclar las coordenadas locales de una forma en movimiento con las coordenadas del lienzo. Apple documenta tanto la medición de gestos en espacios con nombre como el posicionamiento basado en el centro. coordinateSpace(_:), position(x:y:).

La función compartida projectedCenter también mantiene el rectángulo dentro del lienzo. La vista previa y la confirmación usan los mismos límites: centros horizontales entre 40 y 560, y centros verticales entre 30 y 330. En un borde, el movimiento sigue basándose en el desplazamiento total desde el inicio original del arrastre; el rectángulo se aleja del borde cuando la posición propuesta vuelve a quedar dentro de los límites.

Un movimiento inferior al umbral de reconocimiento configurado de 3 puntos no constituye un arrastre reconocido. En este ejemplo, el inicio se considera deliberadamente el comienzo de una interacción reconocida para mover la forma. El requisito de capturar el estado del modelo en el evento físico de pulsación del ratón corresponde a un límite distinto. Inicializador de gesto de arrastre.

Por qué la posición inicial se conserva durante la actualización final

Apple define DragGesture.Value.translation como la traslación total desde el inicio del gesto hasta el evento actual. Es acumulativa, no un delta desde la devolución de llamada anterior. Por tanto, sumar cada traslación notificada a una posición del modelo ya actualizada contaría el movimiento de más. DragGesture.Value.

Considera estas entradas explícitamente ilustrativas, todas dentro de los límites del lienzo:

Etapa Traslación total notificada Centro confirmado Centro mostrado
Antes del arrastre (0, 0) (160, 140) (160, 140)
Primera actualización (12, 5) (160, 140) (172, 145)
Segunda actualización (30, -10) (160, 140) (190, 130)
Finalización correcta (45, 20) Pasa a ser (205, 160) Se estabiliza en (205, 160)

Todas las vistas previas usan la misma posición de referencia. La finalización aplica una sola vez la (45, 20) final. A continuación, un nuevo arrastre finalizado correctamente con la traslación (-15, 10) parte de (205, 160) y confirma (190, 170). Estos cálculos se comprobaron con un cálculo determinista en Python. La app SwiftUI se contrastó con la documentación de API consultada, pero no se compiló ni se ejecutó en este entorno.

No sustituyas el centro inicial del rectángulo por startLocation. Apple define startLocation como la ubicación del primer evento del arrastre. El puntero puede comenzar cerca de una esquina del rectángulo, por lo que esa ubicación y el centro de la forma en el modelo pueden diferir. Del mismo modo, predictedEndTranslation es una predicción basada en la velocidad; para esta interacción de colocación directa, usa translation, el valor final. DragGesture.Value.

En este diseño no hace falta revertir la posición confirmada si se cancela el gesto, porque las actualizaciones de la vista previa nunca la sobrescriben. Apple documenta que el estado del gesto se restablece cuando este queda inactivo, mientras que onEnded se ejecuta solo tras una finalización correcta. Por tanto, el desplazamiento temporal desaparece sin confirmar un movimiento cancelado. Esta conclusión se basa en esos comportamientos documentados; no depende de suponer un orden entre el restablecimiento y la función de retorno de finalización correcta. GestureState, onEnded(_:).

Por qué un monitor de liberación del ratón de AppKit no debería borrar la posición de referencia

Un monitor de eventos local de AppKit recibe los eventos antes de que se distribuyan a la app. Por tanto, borrar el estado compartido de inicio del gesto desde un evento de liberación del ratón monitorizado puede eliminarlo antes de que el procesamiento posterior gestione ese evento. Esto se deriva del límite de distribución documentado, no de una garantía documentada sobre el orden de las funciones de retorno de SwiftUI. addLocalMonitorForEvents(matching:handler:).

Asimismo, NSResponder.mouseUp(with:) indica que el usuario soltó el botón izquierdo del ratón. Su contrato no declara que todas las funciones de retorno de gestos de SwiftUI ni todas las actualizaciones de vistas hayan terminado. Mantén la observación de eventos sin procesar y la finalización de gestos reconocidos como responsabilidades distintas. mouseUp(with:).

Las API revisadas aquí no establecen un significado general para las funciones de retorno que indique que SwiftUI terminó de procesar todo lo relacionado con un evento sin procesar. Un bloque de limpieza en cola o un temporizador breve añadirían una suposición de planificación sin establecer esa relación. Para esta tarea, la opción respaldada es realizar el cálculo final del modelo dentro de onEnded, donde está disponible el valor final del gesto.

Si la interacción debe implementarse en una vista personalizada de AppKit, deja que esa vista gestione su secuencia de entrada y el estado de la sesión, e intégrala mediante NSViewRepresentable. Apple proporciona puntos de extensión para crear, actualizar, coordinar y desmontar esa integración. Mantén la sesión de la vista representada en la vista de AppKit o en su coordinador, en lugar de reconstruirla a partir de cada valor de la vista representable. SwiftUI controla el diseño de la vista representada, así que no cambies por tu cuenta el marco ni los límites que gestiona. NSViewRepresentable.

¿Cuándo necesita la interacción una instantánea explícita?

La posición confirmada del ejemplo es suficiente porque ninguna otra acción la cambia mientras el arrastre está activo. Para un lienzo más amplio, usa esta guía de decisión antes de introducir un objeto de sesión independiente:

Requisito Consecuencia para el diseño
Mover un elemento cuya posición confirmada permanece sin cambios durante la vista previa Usa el enfoque mostrado de posición de referencia más vista previa
Mover juntos varios elementos seleccionados Conserva sus identificadores estables y posiciones iniciales en una sola instantánea de interacción
Permitir que otros comandos editen posiciones durante el arrastre Define si bloquearás esas ediciones, cancelarás el arrastre o resolverás el conflicto usando una revisión del modelo
Capturar el estado en la pulsación física del ratón antes del reconocimiento Usa un límite de entrada de AppKit con un responsable explícito; la primera actualización del gesto no equivale a ello
Mantener una sesión independiente después de la finalización Asígnale un responsable explícito y una política de terminación, en lugar de depender del estado temporal del gesto

Una instantánea debe conservar valores. Las estructuras de Swift tienen semántica de valor, mientras que asignar una instancia de clase copia su referencia. Por tanto, una matriz de referencias a objetos de elemento mutables no congela por sí sola sus posiciones. Para mover varios elementos, captura los identificadores y los valores de coordenadas necesarios para el cálculo final. Estructuras y clases — The Swift Programming Language.

Si introduces una sesión persistente, define explícitamente la finalización correcta, la cancelación y la eliminación de la vista. Borrarla solo en onEnded deja sin resolver la cancelación, porque esa función de retorno solo se ejecuta tras una finalización correcta. No reutilices la transacción de restablecimiento del estado del gesto como una notificación universal de finalización: Apple documenta que la función de restablecimiento proporciona una transacción para restablecer el estado del gesto. onEnded(_:), init(wrappedValue:reset:).

Para la tarea habitual de mover un rectángulo, conserva la posición de referencia dejando intacto el modelo confirmado durante la vista previa. Calcula la posición final a partir del valor propio del gesto finalizado correctamente, confírmala una sola vez y deja que se restablezca el estado temporal del gesto. Así, la posición inicial tiene un responsable y una duración claros hasta completar el cálculo que la necesita.

Fuentes