Usa un Button de SwiftUI cuando tu superficie personalizada para Mac ejecute una acción con nombre. Usa un gesto de clic cuando la interacción necesite información del gesto, como la ubicación en un lienzo. Esta guía crea un pequeño editor de formas para mostrar dónde corresponde cada opción.
Apple recomienda explícitamente usar un botón siempre que un elemento cumpla esa función. Un botón puede usar cualquier vista como etiqueta, así que darle una apariencia personalizada no requiere reemplazar su comportamiento de interacción. SwiftUI proporciona comportamientos de botón estándar, incluidas etiquetas y sugerencias de accesibilidad. Gestures
¿Qué interacción necesita tu superficie?
Para quien desarrolla una app para Mac, la pregunta útil es qué información debe proporcionar la interacción. Un mosaico con la etiqueta Add circle expresa un comando. Hacer clic en una superficie de dibujo vacía para colocar un círculo expresa un comando y una ubicación. Esa entrada adicional cambia cuál es la implementación adecuada.
La siguiente guía de decisión aplica las distinciones documentadas por Apple a un editor de formas de escritorio común. Son recomendaciones de implementación derivadas de las API, no reglas para todas las interfaces posibles.
| Interacción que se busca | Opción inicial | Motivo |
|---|---|---|
Hacer clic en un mosaico personalizado con la etiqueta Add circle |
Button con una etiqueta personalizada |
La superficie inicia una acción con nombre. |
| Darle a ese mosaico un fondo distintivo y una apariencia al presionarlo | ButtonStyle |
Personaliza la apariencia y conserva el comportamiento de interacción estándar del botón. |
| Hacer clic en un lienzo para colocar un círculo en ese punto | SpatialTapGesture |
El resultado incluye la ubicación de la interacción. |
| Reconocer dos clics en una vista de contenido sin necesitar coordenadas | onTapGesture(count: 2) |
El modificador acepta una cantidad de clics requerida. |
| Configurar botones específicos del ratón en una interacción de AppKit | NSClickGestureRecognizer |
Permite configurar el botón del ratón y la cantidad de clics. |
Apple documenta el botón como una acción emparejada con una etiqueta, y distingue la personalización de la apariencia mediante ButtonStyle de la personalización de la interacción mediante PrimitiveButtonStyle. Sus API de gestos documentan por separado la cantidad de clics y los resultados espaciales. Button, onTapGesture(count:perform:), SpatialTapGesture, NSClickGestureRecognizer
Por tanto, un mosaico de acción personalizado empieza como un botón. Recurre a un gesto cuando puedas identificar la entrada adicional o el requisito de reconocimiento que necesita la superficie.
¿Cómo puedes crear un botón personalizado sin rehacer su comportamiento?
Pon el comando en el cierre de acción del botón y el diseño en su etiqueta. Usa ButtonStyle para la presentación. Apple documenta que este protocolo conserva el comportamiento de interacción estándar de la plataforma; PrimitiveButtonStyle es el punto de extensión independiente para cambiar intencionalmente el comportamiento de interacción. ButtonStyle
El ejemplo siguiente contiene dos superficies hermanas: un botón personalizado Add circle y un lienzo rectangular para colocar formas. Ambos llaman a la misma función de mutación. El botón elige una posición predecible; el lienzo proporciona un punto local. Así se hace visible la diferencia sin introducir archivos, contenido externo ni otra app.
Crea un proyecto de app para macOS con SwiftUI y establece el destino de implementación en macOS 13 o posterior, que es el mínimo documentado para SpatialTapGesture. Sustituye las definiciones generadas de la app y de la vista de contenido por el primer archivo, y añade el segundo archivo al mismo destino. Conserva exactamente una declaración @main para la app. El código se ha inspeccionado, pero no se ha compilado ni ejecutado en un entorno con el SDK de macOS. SpatialTapGesture
La aplicación completa consta de estos dos archivos. Primero, ShapeSurfaceApp.swift:
import SwiftUI
import Foundation
@main
struct ShapeSurfaceApp: App {
var body: some Scene {
WindowGroup {
ShapeSurfaceView()
}
}
}
private struct PlacedCircle: Identifiable {
let id = UUID()
let center: CGPoint
}
struct ShapeSurfaceView: View {
@State private var circles: [PlacedCircle] = []
private let limit = 6
private var isFull: Bool {
circles.count >= limit
}
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Button {
let x = 35 + CGFloat(circles.count) * 50
addCircle(at: CGPoint(x: x, y: 100))
} label: {
HStack(spacing: 12) {
Circle()
.fill(Color.blue)
.frame(width: 24, height: 24)
Text("Add circle")
}
.frame(width: 280, height: 64)
.contentShape(RoundedRectangle(cornerRadius: 12))
}
.buttonStyle(SurfaceButtonStyle())
.keyboardShortcut("a", modifiers: [.command, .shift])
.disabled(isFull)
.opacity(isFull ? 0.4 : 1)
Text("Click the canvas to place a circle. Maximum: 6.")
ZStack(alignment: .topLeading) {
Color.gray.opacity(0.1)
ForEach(circles) { circle in
Circle()
.fill(Color.blue)
.frame(width: 24, height: 24)
.position(circle.center)
}
}
.frame(width: 320, height: 200)
.contentShape(Rectangle())
.gesture(
SpatialTapGesture()
.onEnded { value in
addCircle(at: value.location)
}
)
.disabled(isFull)
.border(Color.gray)
.accessibilityElement(children: .ignore)
.accessibilityLabel("Circle canvas")
.accessibilityValue("\(circles.count) of \(limit) circles")
Text(isFull
? "Six circles placed. Clear to start again."
: "\(circles.count) of \(limit) circles placed")
Button("Clear circles") {
circles.removeAll()
}
.disabled(circles.isEmpty)
}
.padding(24)
}
private func addCircle(at point: CGPoint) {
guard !isFull else { return }
// Keep each 24-point circle inside the 320-by-200 canvas.
let center = CGPoint(
x: min(max(point.x, 12), 308),
y: min(max(point.y, 12), 188)
)
circles.append(PlacedCircle(center: center))
}
}
Segundo, SurfaceButtonStyle.swift:
import SwiftUI
struct SurfaceButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color.blue.opacity(
configuration.isPressed ? 0.25 : 0.1
))
)
}
}
El estilo lee configuration.isPressed para dibujar la respuesta visual; nunca ejecuta la acción. Apple define esa propiedad como el estado de pulsación actual del botón. Así, la representación y la ejecución de la acción quedan en lugares separados. isPressed
El límite de seis círculos es una regla ilustrativa de la aplicación. Ambos puntos de entrada usan addCircle(at:), que comprueba la capacidad antes de añadir uno. La apariencia desactivada comunica el límite, mientras que la guarda compartida mantiene coherente la regla de mutación. Apple documenta disabled(_:) como el control de la interacción con una vista, incluidos los clics y los gestos. disabled(_:)
¿Por qué usa el lienzo un gesto de clic espacial?
El lienzo necesita saber dónde hizo clic el usuario. La sobrecarga básica de onTapGesture(count:perform:) proporciona una acción sin un parámetro de ubicación. SpatialTapGesture informa explícitamente de una ubicación, y su inicializador usa de forma predeterminada una interacción en el espacio de coordenadas local. onTapGesture(count:perform:), SpatialTapGesture, init(count:coordinateSpace:)
En este programa, el gesto pertenece al lienzo de tamaño fijo, cuyas coordenadas locales determinan la posición de los círculos. La función de colocación limita cada centro para dejar un margen de 12 puntos, igual al radio del círculo. Por ello, al hacer clic cerca de un borde, el círculo completo queda dentro del lienzo. Se permiten círculos superpuestos, y la cantidad sigue visible aunque las formas se solapen.
El botón y el lienzo también tienen intencionalmente comportamientos distintos con el teclado. El atajo del botón ejecuta la misma acción que activarlo: añade un círculo en la siguiente posición. No genera una ubicación del puntero. Apple documenta que los atajos de teclado invocan la acción principal de un control y señala que, si se asigna el mismo atajo varias veces, se resuelve con el primer control coincidente que se encuentre. Asigna este atajo una sola vez en la ventana de ejemplo. keyboardShortcut(_:modifiers:)
La etiqueta de accesibilidad y el recuento del lienzo describen su contenido; no proporcionan un flujo de trabajo con teclado para colocar formas en coordenadas arbitrarias. El botón ofrece una forma de añadir un círculo en una posición predefinida sin usar el puntero. Si también se debe poder colocar con precisión mediante controles de teclado, añade una interfaz explícita para editar la posición, en lugar de suponer que un gesto o una etiqueta descriptiva la proporciona.
¿Cómo debe coincidir la detección de toques con la superficie personalizada?
Define el área de interacción prevista después de establecer el tamaño de la etiqueta. En el ejemplo, la etiqueta del botón tiene un marco de 280 por 64, seguido de un contentShape rectangular con esquinas redondeadas. El lienzo establece un marco de 320 por 200 y luego declara una forma de contenido rectangular. Apple define contentShape(_:eoFill:) como la forma que se usa para detectar toques. contentShape(_:eoFill:)
Esto establece límites de interacción explícitos para las superficies visibles. Si durante un rediseño sacas el marco de la etiqueta del botón, vuelve a revisar el resultado: en el ejemplo, el tamaño y la forma de contenido deben permanecer juntos dentro de la etiqueta. Mantén intencionalmente la geometría del dibujo, el diseño y la interacción, en vez de arreglar un margen que no responde añadiendo otro controlador de acción.
Cuando una superficie deja de responder, revisa estas condiciones en orden:
- Comprueba la geometría. Haz clic en el texto de la etiqueta, el gráfico y su interior vacío. Confirma que la forma de contenido describe los límites previstos.
- Comprueba los contenedores antecesores desactivados. Un contenedor desactivado invalida a un descendiente habilitado, así que un
.disabled(false)local puede no restablecer la interacción. disabled(_:) - Comprueba las superposiciones decorativas. Si una vista es puramente visual, puedes excluirla de la detección de toques con
.allowsHitTesting(false). Aplícalo al elemento decorativo, no a la superficie interactiva. allowsHitTesting(_:) - Comprueba si hay controladores que compiten. Identifica qué vista es responsable de la acción antes de cambiar la prioridad de los gestos.
¿Cómo puedes evitar controladores que compiten y errores de responsabilidad?
Usa un único controlador para cada vía de activación prevista. En el ejemplo, el botón personalizado es responsable de su comando y el lienzo se encarga de la colocación basada en la ubicación. Ninguno está dentro de un padre que tenga otro gesto de colocación. Esta estructura facilita comprobar el efecto previsto de un clic.
Evita adjuntar un segundo controlador de toque al botón solo para llamar a la misma función de mutación. Del mismo modo, no añadas un gesto al padre de varios controles sin decidir si deben ocurrir las interacciones del padre y del hijo. El ejemplo de simultaneousGesture de Apple muestra que pueden ejecutarse tanto los controladores del padre como los del hijo. En cambio, highPriorityGesture da mayor prioridad al gesto adjunto. Estos modificadores expresan una política de interacción; no son soluciones intercambiables. simultaneousGesture(_:including:), highPriorityGesture(_:including:)
Mantén el estado de la aplicación en la vista que es responsable de él. Aquí, ShapeSurfaceView almacena los círculos ficticios, y el estilo solo representa la etiqueta proporcionada. Apple recomienda mantener el estado privado en la vista de nivel más alto que necesite acceder a él, y pasar valores o enlaces a las vistas descendientes cuando corresponda. State
Si más adelante un adaptador de AppKit almacena una devolución de llamada, inspecciona por separado las relaciones de captura. Los cierres de Swift pueden capturar valores del contexto; una clase que almacena un cierre que captura esa misma instancia puede crear un ciclo de referencias fuertes. Elige una lista de captura según la duración real de los objetos. Cambiar un botón por un gesto no resuelve la propiedad de las devoluciones de llamada. Closures
¿Cuándo es AppKit el siguiente paso adecuado?
Considera AppKit cuando tu superficie necesite la configuración específica del ratón que documenta. NSClickGestureRecognizer admite una cantidad de clics requerida y una máscara de botones del ratón. Apple especifica que el bit cero representa el botón principal y el bit uno el secundario; el reconocimiento requiere los clics configurados sin arrastrar. También habilita automáticamente los retrasos de eventos para los botones del ratón configurados. NSClickGestureRecognizer, buttonMask
Elige la vía de integración según tu destino de implementación y el componente del que eres responsable:
- En macOS 26 o posterior,
NSGestureRecognizerRepresentableintegra directamente un reconocedor de AppKit en una jerarquía de SwiftUI mediante.gesture(...). SwiftUI instala el destino de la acción; implementa el controlador de acción del representable y usa su conversor de contexto para convertir coordenadas. Apple advierte que la vista asociada al reconocedor puede tener una geometría distinta de la vista de SwiftUI. NSGestureRecognizerRepresentable - Para una vista de AppKit alojada en SwiftUI,
NSViewRepresentablegestiona la creación, las actualizaciones y la eliminación de tu vista. Usa su coordinador para las devoluciones de llamada y deja que SwiftUI controle el diseño de la vista representada. Esta API está disponible desde macOS 10.15. NSViewRepresentable
Al añadir cualquiera de los dos adaptadores, conserva el límite de eventos. AppKit documenta que un reconocedor actúa sobre la vista a la que está conectado y sus descendientes, recibe los eventos pertinentes antes que la vista y puede retrasar su entrega. También indica explícitamente que no admite transferir el seguimiento de eventos a otros mecanismos, como arrastrar y soltar o los menús emergentes. Diseña esas interacciones con los mecanismos compatibles en lugar de tratar un reconocedor de clics como un retransmisor universal de eventos. NSGestureRecognizer
¿Qué debes comprobar en la interacción terminada para Mac?
Compila el ejemplo en Xcode y comprueba el comportamiento observable antes de adaptarlo a una jerarquía de vistas más grande:
- Haz clic en el texto y en el interior vacío del botón personalizado. Cada activación debe añadir un círculo y actualizar el recuento una vez.
- Pulsa
Command–Shift–Aen la ventana activa. Debe ejecutar la acción del botón basada en la posición. - Haz clic en distintos puntos del lienzo, incluidos los cercanos a sus bordes. Los círculos deben seguir las coordenadas locales con el margen documentado en el código.
- Llega a seis círculos. Ambas vías de colocación deben dejar de añadirlos, mientras que
Clear circlessigue disponible. - Borra el lienzo. La colocación debe volver a estar disponible.
- Inspecciona el botón con VoiceOver y prueba la navegación por teclado con la configuración actual de tu Mac. Verifica que, tras aplicar los estilos, la acción siga siendo comprensible.
Para una superficie de acción personalizada para Mac, empieza por el significado de la interacción: un comando con nombre corresponde a un Button, la personalización visual corresponde a su etiqueta y estilo, y la colocación que depende de una ubicación corresponde al gesto del lienzo. Conecta esas vías mediante una única función que cambie el estado y añade AppKit solo cuando su configuración adicional responda a un requisito concreto.
Fuentes
Button — Documentación para desarrolladores de Apple.
Gestures — Documentación para desarrolladores de Apple.
ButtonStyle — Documentación para desarrolladores de Apple.
isPressed — Documentación para desarrolladores de Apple.
onTapGesture(count:perform:) — Documentación para desarrolladores de Apple.
SpatialTapGesture — Documentación para desarrolladores de Apple.
init(count:coordinateSpace:) — Documentación para desarrolladores de Apple.
contentShape(_:eoFill:) — Documentación para desarrolladores de Apple.
disabled(_:) — Documentación para desarrolladores de Apple.
allowsHitTesting(_:) — Documentación para desarrolladores de Apple.
keyboardShortcut(_:modifiers:) — Documentación para desarrolladores de Apple.
simultaneousGesture(_:including:) — Documentación para desarrolladores de Apple.
highPriorityGesture(_:including:) — Documentación para desarrolladores de Apple.
State — Documentación para desarrolladores de Apple.
Closures — Swift.org, The Swift Programming Language.
NSClickGestureRecognizer — Documentación para desarrolladores de Apple.
buttonMask — Documentación para desarrolladores de Apple.
NSGestureRecognizer — Documentación para desarrolladores de Apple.
NSGestureRecognizerRepresentable — Documentación para desarrolladores de Apple.
NSViewRepresentable — Documentación para desarrolladores de Apple.