Para hacer clic en un control de SwiftUI a través de un hueco transparente en una vista superpuesta, asigna a la vista superior una forma explícita para la detección de impactos que excluya el hueco. Este recorrido crea esa interacción dentro de una app para Mac, usando un anillo en memoria y un botón debajo.

La tarea concreta es una pequeña interfaz de dibujo para escritorio: al hacer clic en el anillo naranja, se selecciona el anillo; al hacer clic en su centro transparente, se activa el control azul visible debajo. Ambas capas pertenecen a la misma jerarquía de vistas de SwiftUI. El ejemplo usa geometría compartida para el dibujo y la detección de impactos, por lo que no necesita archivos de imagen ni muestreo de píxeles.

Elige la regla de detección de impactos antes de cambiar el manejo de eventos

Apple documenta dos controles de SwiftUI pertinentes. allowsHitTesting(_:) determina si una vista participa en la detección de impactos. contentShape(_:eoFill:) especifica la forma usada para esa detección. Usa el primero para elementos decorativos que nunca deban recibir clics; usa el segundo para una superposición interactiva con huecos. La documentación de Apple sobre la participación en la detección de impactos y la documentación sobre la forma del contenido describen estas responsabilidades por separado.

Requisito de la capa superior Opción de implementación
El elemento decorativo debe ignorar todos los clics Aplica .allowsHitTesting(false) a ese elemento decorativo
Un círculo debe responder dentro de su contorno Asigna una forma de contenido circular a su vista interactiva
Un anillo debe responder alrededor de un centro transparente Usa una forma compuesta y el relleno par-impar para el dibujo y la detección de impactos
Un subárbol de AppKit propio necesita una participación selectiva Sobrescribe su límite hitTest(_:) y rechaza los puntos fuera de su región de interacción

Estas API describen la participación y la geometría; no especifican una comprobación automática de la opacidad del píxel finalmente renderizado. Para formas sintéticas, la opción admitida es describir explícitamente la región de interacción deseada. Por eso, una apariencia transparente por sí sola no basta para especificar la interacción. Esta distinción se desprende de las interfaces documentadas, basadas en valores booleanos y formas, y no de una garantía sobre todos los modificadores de renderizado.

Crea un ejemplo completo de un anillo sobre un botón en SwiftUI

Crea en Xcode una app para macOS con SwiftUI y establece macOS 12 o posterior como destino de implementación. Sustituye la declaración de la app y la vista inicial generadas por el programa completo que aparece abajo, dejando una sola declaración @main en el destino. El ejemplo usa .contentShape(.interaction, …), cuyo tipo de interacción está disponible desde macOS 12 y controla explícitamente la detección de impactos. La documentación de Apple sobre interacciones describe ese comportamiento y su disponibilidad.

El anillo contiene dos subrutas elípticas concéntricas. Tanto el dibujo como la interacción usan la regla par-impar, que convierte la elipse interior en un hueco. Apple documenta Shape.path(in:) como la geometría dentro del rectángulo proporcionado y FillStyle como la configuración de renderizado, incluido el relleno par-impar. La geometría de las formas y FillStyle definen los contratos correspondientes.

import SwiftUI

struct RingShape: Shape {
    func path(in rect: CGRect) -> Path {
        let side = min(rect.width, rect.height)
        let outer = CGRect(
            x: rect.midX - side / 2,
            y: rect.midY - side / 2,
            width: side,
            height: side
        )
        let inner = outer.insetBy(
            dx: side * 0.30,
            dy: side * 0.30
        )

        var path = Path()
        path.addEllipse(in: outer)
        path.addEllipse(in: inner)
        return path
    }
}

struct RoutingDemo: View {
    @State private var lowerClicks = 0
    @State private var ringClicks = 0

    var body: some View {
        VStack(spacing: 18) {
            Text("Click the center or the orange ring")

            ZStack {
                Button {
                    lowerClicks += 1
                } label: {
                    Rectangle()
                        .fill(Color.blue)
                        .frame(width: 240, height: 240)
                        .overlay {
                            Text("Below")
                                .foregroundColor(.white)
                        }
                        .contentShape(Rectangle())
                }
                .buttonStyle(.plain)

                RingShape()
                    .fill(Color.orange, style: FillStyle(eoFill: true))
                    .frame(width: 240, height: 240)
                    .contentShape(.interaction, RingShape(), eoFill: true)
                    .onTapGesture {
                        ringClicks += 1
                    }

                Rectangle()
                    .stroke(Color.gray, lineWidth: 1)
                    .frame(width: 240, height: 240)
                    .allowsHitTesting(false)
            }
            .frame(width: 240, height: 240)

            Text("Below: \(lowerClicks) | Ring: \(ringClicks)")
        }
        .padding(24)
    }
}

@main
struct TransparentRoutingApp: App {
    var body: some Scene {
        WindowGroup {
            RoutingDemo()
        }
    }
}

El botón inferior sigue siendo responsable de su propia acción. El gesto del anillo incrementa otro contador. Esas clausuras finales expresan directamente las dos acciones; la guía del lenguaje Swift documenta las clausuras como funcionalidad capaz de capturar valores del ámbito circundante. The Swift Programming Language: Closures explica la sintaxis y el comportamiento de captura.

El contorno gris es decorativo, así que su detección de impactos se desactiva individualmente. Mantén ese modificador en el contorno: aplicarlo a un contenedor que incluya las capas interactivas daría al contenedor una política de participación incorrecta. El anillo debe seguir siendo apto para la interacción en su banda naranja.

Por qué la misma regla de relleno importa dos veces

La llamada de dibujo y el modificador de forma del contenido reciben eoFill: true. Configurarlo solo para el dibujo describiría el hueco visible sin asignar explícitamente la misma regla a la detección de impactos. Apple documenta el parámetro de la forma del contenido por separado del renderizado, por lo que ambas declaraciones forman parte de este ejemplo. El modificador de forma del contenido define la forma de interacción y su regla de orientación.

Mantén también alineados las dimensiones y las transformaciones. Aquí, el dibujo del anillo y su forma de contenido comparten un cuadrado de 240 puntos. Si después mueves o escalas toda la vista del anillo, mantén juntas esas operaciones. Si, en cambio, modificas solo una ruta de dibujo dentro de un contenedor más grande, actualiza la geometría de interacción para que describa la misma región deseada.

Verifica el enrutamiento con tres clics concretos

Activa la ventana de la app y luego haz clic lejos de los límites de las formas. Los siguientes puntos son coordenadas locales ilustrativas dentro del cuadrado de 240 puntos, medidas desde su esquina superior izquierda. Permiten distinguir el hueco central, la banda naranja y la esquina transparente sin depender de los píxeles de los bordes suavizados.

Punto Geometría visible de la capa superior Cambio esperado del contador
(120, 120) Hueco central Below aumenta una vez
(210, 120) Banda naranja Ring aumenta una vez
(10, 10) Fuera del círculo exterior del anillo Below aumenta una vez

El radio exterior es de 120 puntos. El desplazamiento hacia dentro de la elipse interior es de 72 puntos en cada lado, lo que deja un radio interior de 48 puntos. Los tres puntos de prueba están a 0, 90 y aproximadamente 155,6 puntos del centro, respectivamente. Comprobé este cálculo geométrico determinista con Python; inspeccioné la app de SwiftUI, pero no pude compilarla ni ejecutarla en este entorno porque no había una cadena de herramientas de Swift disponible.

Esta prueba también sirve como secuencia de diagnóstico. Si la banda naranja no responde, revisa el gesto del anillo y sus modificadores de participación. Si el centro activa el anillo, revisa la forma del contenido y el parámetro par-impar. Si ninguna capa responde en el hueco, quita temporalmente las superposiciones adicionales y luego restáuralas de una en una para identificar cuál cambia el enrutamiento.

Para comprobar directamente la geometría en tu propio código, Path.contains(_:eoFill:) admite la misma opción par-impar. Pasa un punto expresado en el sistema de coordenadas de la ruta que construiste. Así, una prueba geométrica puede plantear la misma pregunta sobre si un punto está dentro o fuera, sin tratar una captura de pantalla como modelo de interacción. La documentación de Apple sobre la contención de rutas describe esta operación.

Usa la detección de impactos de AppKit solo en un límite que controles

Cuando una superposición ya está implementada en AppKit, NSView.hitTest(_:) es el punto de personalización documentado. Devuelve el descendiente seleccionado para un punto y se usa principalmente para elegir el destinatario del evento de pulsación del ratón. El punto de entrada está en las coordenadas de la supervista del receptor, así que conviértelo antes de compararlo con la geometría local. La documentación de Apple sobre hitTest de NSView especifica estos detalles.

En una vista personalizada propia, la sobrescritura puede rechazar los puntos que estén fuera de su región de interacción y delegar los puntos aceptados en super.hitTest(_:). Lo siguiente es un fragmento de método para una subclase personalizada de NSView existente, no una segunda app completa. Usa RingShape, definida arriba, y requiere import AppKit junto con import SwiftUI.

override func hitTest(_ point: NSPoint) -> NSView? {
    guard let parent = superview else {
        return super.hitTest(point)
    }

    let localPoint = convert(point, from: parent)
    let region = RingShape().path(in: bounds)

    guard region.contains(localPoint, eoFill: true) else {
        return nil
    }

    return super.hitTest(point)
}

Usa este fragmento solo cuando el contenido visible de la vista personalizada y su subárbol sigan realmente esa geometría de anillo. Devolver nil rechaza ese subárbol para el punto; no indica un destino ni ordena a SwiftUI activar un elemento hermano concreto. Que la capa inferior deseada pueda recibir el clic sigue dependiendo de la jerarquía circundante. Una vista contenedora que siga aceptando el punto puede continuar siendo un obstáculo.

Al integrar esa vista de AppKit propia en SwiftUI, usa NSViewRepresentable para crearla y actualizar su configuración. Mantén el estado de los elementos y la geometría de interacción sincronizados mediante el método de actualización del representable; usa un coordinador al conectar mensajes de delegado o de destino y acción. Apple también indica a los desarrolladores que dejen frame y bounds de la vista raíz representada bajo el control de diseño de SwiftUI. La documentación de Apple sobre NSViewRepresentable describe estos límites del ciclo de vida y la propiedad.

Conserva el evento original y el responsable del gesto

Toma la decisión de enrutamiento durante la detección de impactos. Evita capturar un clic en la capa superior y llamar manualmente a la acción del control inferior: eso sustituiría la interacción normal del control inferior por un comando de la app. Del mismo modo, este ejemplo no necesita reenviar eventos, recorrer las vistas de implementación de SwiftUI ni sustituir el controlador de gestos de una capa inferior. Cada capa interactiva visible es responsable de su propio comportamiento.

El ámbito de este caso se limita a la activación normal con el botón principal dentro de la jerarquía de vistas de la app. Que un clic en el centro funcione no establece el comportamiento para el desplazamiento, el arrastre, el paso del puntero ni los menús contextuales; pruébalos por separado si tu interfaz los añade. En particular, Apple distingue la forma de contenido de interacción de las formas de vista previa de arrastre y de vista previa de menú contextual, que no cambian la detección de impactos. La documentación de Apple sobre la forma del contenido explica esos tipos por separado.

Para esta tarea de escritorio, mantén la ruta del anillo como definición compartida de su banda visible y su región clicable. Así, el centro transparente tiene un significado preciso: la capa superior rechaza el punto y el control inferior de SwiftUI sigue siendo responsable de responder mediante el sistema de interacción normal.

Fuentes