Añade el gesto de arrastre del elemento principal con simultaneousGesture(_:including:) para que pueda participar junto con las interacciones de los elementos secundarios. Mantén las acciones de clic en los botones secundarios, establece un umbral de movimiento para el arrastre y decide explícitamente si la selección puede producirse al mismo tiempo que un arrastre.

Este tutorial crea un panel móvil que contiene tres botones de elementos sintéticos. La tarea del lector es concreta: al hacer clic en un elemento, se selecciona; al arrastrar el panel, cambia su posición dentro de la misma app para Mac. El ejemplo permite que la selección y el movimiento coexistan; una variante con un control de arrastre separado ofrece regiones de interacción distintas cuando eso resulta preferible.

Elige el contrato de interacción antes de cambiar la prioridad de los gestos

Apple documenta simultaneousGesture(_:including:) como un modificador que añade un gesto con la misma prioridad que los gestos existentes. Su ejemplo muestra que tanto el controlador de toque de un elemento secundario como el controlador de toque del contenedor que lo envuelve responden a un clic. Este es el mecanismo de composición pertinente cuando el elemento principal debe participar sin imponerse deliberadamente al elemento secundario. Documentación de Apple sobre gestos simultáneos

Sin embargo, el reconocimiento simultáneo no expresa por sí solo la regla «el arrastre debe cancelar todas las acciones de los elementos secundarios». El ejemplo de Apple tampoco demuestra que todos los estilos de botón y todos los contenedores arbitren del mismo modo entre un botón y un arrastre. Trata la participación de los gestos y el comportamiento de la app como decisiones distintas.

En este panel, el elemento principal es responsable de la posición del panel y del elemento seleccionado. Cada elemento secundario contiene un botón que solicita la selección mediante un cierre. La solicitud de selección sigue siendo válida aunque la misma interacción con el puntero mueva el panel. Si el comportamiento requerido prohíbe la activación durante el movimiento, usa la variante con un control de arrastre separado que aparece más abajo, en vez de depender de un orden de llamadas no documentado.

Usa un Button para la acción del elemento secundario. Apple recomienda explícitamente los botones cuando una interacción requiere el comportamiento propio de un botón, incluidas las funciones de accesibilidad estándar. Una forma personalizada puede seguir siendo la etiqueta del botón; no es necesario convertirla en un gesto de toque independiente. Documentación de Apple sobre gestos

Crea un panel móvil completo con botones secundarios

Crea una app SwiftUI para macOS en Xcode, establece como destino de implementación macOS 14 o posterior y sustituye la app y la vista de contenido generadas por el siguiente archivo único. Mantén un solo punto de entrada de la app en el destino. El requisito de macOS 14 cubre el modificador moderno coordinateSpace(_:) que se usa aquí. Documentación de Apple sobre espacios de coordenadas

Todos los elementos y el estado existen en memoria. El ejemplo mueve el panel completo, incluidos sus botones, dentro de un área fija. No implementa el traslado de elementos ni mueve la ventana de la aplicación.

import SwiftUI

@main
struct GesturePanelApp: App {
    var body: some Scene {
        WindowGroup {
            PanelDemo()
        }
    }
}

private struct DemoItem: Identifiable {
    let id: Int
    let title: String
    let color: Color
}

private struct ItemTarget: View {
    let item: DemoItem
    let isSelected: Bool
    let select: () -> Void

    var body: some View {
        Button(action: select) {
            HStack(spacing: 10) {
                Circle()
                    .fill(item.color)
                    .frame(width: 18, height: 18)
                Text(item.title)
                Spacer()
                if isSelected {
                    Text("Selected")
                        .font(.caption)
                }
            }
            .padding(12)
            .frame(width: 220)
            .background(
                isSelected
                    ? Color.accentColor.opacity(0.18)
                    : Color.gray.opacity(0.12)
            )
            .contentShape(Rectangle())
        }
        .buttonStyle(.plain)
    }
}

private struct PanelDemo: View {
    private let items = [
        DemoItem(id: 1, title: "Item A", color: .blue),
        DemoItem(id: 2, title: "Item B", color: .orange),
        DemoItem(id: 3, title: "Item C", color: .purple)
    ]

    @State private var selectedID: Int? = nil
    @State private var committedOffset: CGSize = .zero
    @GestureState private var liveTranslation: CGSize = .zero

    private var pan: some Gesture {
        DragGesture(
            minimumDistance: 8,
            coordinateSpace: .named("stage")
        )
        .updating($liveTranslation) { value, translation, _ in
            translation = value.translation
        }
        .onEnded { value in
            committedOffset = CGSize(
                width: committedOffset.width + value.translation.width,
                height: committedOffset.height + value.translation.height
            )
        }
    }

    var body: some View {
        VStack(spacing: 12) {
            Text("Click an item or drag the panel")

            ZStack {
                Color.gray.opacity(0.06)

                VStack(spacing: 10) {
                    Text("Drag panel")
                        .font(.headline)
                        .frame(width: 244, height: 32)
                        .contentShape(Rectangle())

                    ForEach(items) { item in
                        ItemTarget(
                            item: item,
                            isSelected: selectedID == item.id,
                            select: { selectedID = item.id }
                        )
                    }
                }
                .padding(16)
                .background(Color.gray.opacity(0.15))
                .contentShape(Rectangle())
                .simultaneousGesture(pan, including: .all)
                .offset(
                    x: committedOffset.width + liveTranslation.width,
                    y: committedOffset.height + liveTranslation.height
                )
            }
            .frame(width: 640, height: 420)
            .coordinateSpace(.named("stage"))

            Button("Reset panel") {
                committedOffset = .zero
            }
        }
        .padding(20)
    }
}

Este programa se ha revisado comparándolo con la documentación de API consultada, pero aquí no se ha compilado ni probado de forma interactiva: este entorno no dispone de un compilador de Swift ni de un entorno de ejecución de la interfaz de macOS. Compílalo en Xcode y realiza las comprobaciones siguientes en las versiones de macOS que admitas.

Por qué el arrastre tiene un umbral distinto de cero

DragGesture expone una distancia mínima de movimiento que debe superarse para que el gesto se reconozca. El valor ilustrativo 8 ofrece cierta tolerancia al movimiento de los clics normales antes de que el elemento principal empiece a arrastrarse. Es una decisión de la aplicación, no un umbral prescrito por Apple ni una política completa para distinguir entre clic y arrastre. Un arrastre con distancia cero elimina esa distinción concreta. Documentación de Apple sobre DragGesture

Por qué el movimiento transitorio y el confirmado son independientes

Durante el reconocimiento, updating(_:body:) escribe la traslación actual en liveTranslation. Apple documenta que este estado del gesto se restablece cuando el gesto termina o se cancela. El ejemplo confirma el desplazamiento final solo en onEnded, por lo que una cancelación descarta la vista previa aún no confirmada en vez de dejar una traslación temporal obsoleta. Documentación de Apple sobre updating

La posición mostrada es el desplazamiento confirmado más la traslación actual. Apple define translation como el desplazamiento total desde el inicio del arrastre, por lo que sumarlo repetidamente al desplazamiento confirmado en cada cambio contaría el movimiento de más. A modo de ejemplo, un desplazamiento horizontal confirmado de 40 y una traslación actual de 12 producen un desplazamiento mostrado de 52; al completar el arrastre, 52 se confirma una sola vez. Documentación de Apple sobre translation

El espacio de coordenadas con nombre pertenece al área fija que rodea el panel móvil. Así, el cálculo del movimiento se refiere a una región de referencia explícita. Los botones secundarios no mantienen copias propias de la posición del panel.

Por qué la selección permanece en el elemento principal

El elemento principal almacena selectedID y proporciona a cada elemento secundario su estado de selección actual y un cierre para seleccionar. Esto sigue la recomendación de Apple de mantener el estado en la vista de nivel más alto que lo necesite y, después, pasar valores o enlaces a las vistas descendientes. Documentación de Apple sobre State

El cierre captura el elemento actual y solicita una actualización del estado del elemento principal. Swift permite que los cierres capturen valores del contexto que los rodea; no es necesario copiar ni reenviar ningún evento de AppKit para implementar esa llamada. El lenguaje de programación Swift: Closures

Comprende las regiones de interacción y las máscaras de gestos

contentShape(Rectangle()) define la forma usada para las pruebas de interacción. En la etiqueta del botón, aparece después del relleno y del marco para dejar explícita la región rectangular prevista de la etiqueta. En el elemento principal, define la forma de interacción del panel. No establece la prioridad de los reconocedores ni exige que dos gestos cooperen. Documentación de Apple sobre contentShape

La máscara que se proporciona a un modificador de gesto determina qué gestos siguen habilitados. Es fácil interpretar mal estas opciones: subviews significa que el gesto añadido al elemento principal está deshabilitado, no que coopere automáticamente con los elementos descendientes. Documentación de Apple sobre GestureMask

Máscara Efecto documentado Idoneidad en este caso
.all Habilita el gesto añadido y otros gestos en la vista y sus subvistas Punto de partida para la participación compartida
.gesture Habilita el gesto añadido y deshabilita los gestos en la jerarquía de subvistas Entra en conflicto con la conservación de los gestos secundarios
.subviews Habilita los gestos de las subvistas y deshabilita el gesto añadido Deshabilita el arrastre del elemento principal
.none Deshabilita los gestos en la jerarquía de subvistas, incluido el gesto añadido Deshabilita la interacción prevista

Evita tratar highPriorityGesture como una solución general cuando faltan llamadas al arrastre del elemento principal. Su propósito documentado es dar mayor precedencia al gesto adjunto. El ejemplo de Apple muestra que el elemento principal se impone al toque de un elemento secundario. Elígelo cuando esa precedencia sea el comportamiento previsto. Documentación de Apple sobre gestos de alta prioridad

Usa un control de arrastre separado cuando la activación deba ser independiente

Si los botones de un panel no deben compartir una región de arrastre, conserva el mismo estado y el mismo cálculo de movimiento, pero adjunta pan solo al encabezado. Elimina .simultaneousGesture(pan, including: .all) del panel principal.

Sustituye la expresión actual del encabezado por este fragmento dentro del programa completo anterior. Usa la propiedad pan, que ya está definida; no cambia ningún otro requisito previo.

Text("Drag panel")
    .font(.headline)
    .frame(width: 244, height: 32)
    .contentShape(Rectangle())
    .gesture(pan)

Ahora el encabezado y los botones de los elementos ocupan regiones distintas, y los botones ya no tienen el arrastre del panel adjunto a un elemento antecesor. Empezar sobre un elemento produce una interacción con el botón; empezar sobre el encabezado permite mover el panel. Este diseño resulta útil cuando la participación compartida podría hacer ambiguo el resultado.

Mantén el botón nativo cuando el elemento secundario inicie una acción. Apple describe Button como una acción más una etiqueta y permite etiquetas de vista personalizadas, como las que usan los elementos sintéticos de colores. Si lo sustituyes por un controlador de puntero hecho a mano, tendrás que recrear comportamientos que el control ya proporciona. Documentación de Apple sobre Button

Conserva la propiedad de los eventos de AppKit en los límites entre frameworks

Si el panel incluye una vista de AppKit, analiza sus pruebas de interacción por separado de la composición de gestos de SwiftUI. hitTest(_:) de AppKit devuelve el descendiente más profundo que coincide y ayuda a la ventana a determinar quién recibe un evento de pulsación del ratón. El punto de entrada se expresa en las coordenadas de la supervista. Documentación de Apple sobre las pruebas de interacción de AppKit

Devolver un elemento principal desde las pruebas de interacción cambia el destino inicial; no equivale a permitir que un elemento secundario y uno principal se reconozcan simultáneamente. Del mismo modo, devolver nil desde la prueba de interacción de una superposición la excluye de la consideración; no convierte la superposición en un observador pasivo de toda la interacción del elemento secundario.

AppKit documenta que la implementación predeterminada de mouseDown(with:) pasa el mensaje al siguiente respondedor. Ese es el comportamiento de la cadena de respuesta; no es una promesa de que todos los elementos antecesores reciban una copia por separado después de que un control gestione el evento. Conecta la activación del control y el movimiento del panel mediante llamadas o reconocedores explícitos que controles. Documentación de Apple sobre mouseDown

Cuando controlas dos reconocedores de gestos de AppKit y necesitas que se reconozcan simultáneamente, AppKit proporciona gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:). Apple documenta que devolver true permite que el par se reconozca a la vez; devolver false no garantiza que se excluyan si el delegado del otro reconocedor lo permite. Limita esa política al par de reconocedores previsto. Documentación de Apple sobre el delegado de reconocedores

Verifica la interacción con una pequeña matriz de comportamientos

Prueba el estilo exacto del botón, la jerarquía y las versiones de macOS que admita tu app. Las comprobaciones siguientes permiten distinguir los problemas de reconocimiento de los errores aritméticos del movimiento y aclarar si la superficie compartida se ajusta al contrato previsto.

Acción Qué comprobar
Haz clic en cada elemento sin arrastrar Cambia su estado de selección y el panel permanece quieto
Arrastra el encabezado más allá del umbral El panel sigue el puntero y conserva su posición final
Arrastra desde un elemento en la versión compartida El elemento principal se mueve; determina si también se activa el botón y confirma que sea aceptable
Realiza dos arrastres consecutivos El segundo empieza desde la posición confirmada del primero sin acumular cada traslación intermedia
Interrumpe un arrastre activo Se borra la traslación temporal; no se confirma ningún movimiento incompleto
Prueba la variante con control de arrastre Los elementos se activan por separado y el movimiento empieza en el encabezado
Activa los botones de los elementos mediante la navegación con teclado La selección funciona sin necesitar un arrastre con el puntero

Si el espacio vacío del panel se arrastra correctamente, pero las regiones de los elementos no, revisa los controles secundarios y la composición de gestos. Si los clics no funcionan, revisa las máscaras de gestos, la precedencia y las regiones de interacción. Si el panel se acelera o da saltos, revisa la acumulación de traslaciones y el espacio de coordenadas elegido antes de añadir más reconocedores.

El ejemplo deja deliberadamente el movimiento sin límites, por lo que el panel puede salir del área visible; el botón Restablecer panel devuelve el panel a su posición. Para un espacio de trabajo terminado con límites, añade una restricción de posición explícita basada en las dimensiones del área y del panel. Conserva la misma estructura de responsabilidad: los elementos secundarios solicitan acciones, el elemento principal controla el movimiento y el contrato de interacción determina si sus regiones de reconocimiento se superponen.

Fuentes