Para hacer que el espacio vacío dentro de una fila de SwiftUI se pueda pulsar en Mac, expande el contenido de la fila hasta el ancho disponible y aplica una forma de contenido rectangular. Si la fila realiza una acción, coloca ambos modificadores dentro de la etiqueta de un Button.
Este tutorial crea una pequeña app de macOS cuyas filas contienen formas y texto sintéticos. Al pulsar una forma, su etiqueta o el espacio vacío junto a ella, se ejecuta la misma acción de fila. El espacio fuera de la fila queda fuera del área de acción.
¿Por qué responde el texto, pero no el espacio vacío?
El diseño y la detección de pulsaciones responden a preguntas distintas. Un marco determina cómo ocupa una vista el espacio propuesto; una forma de contenido define la región que se usa para detectar pulsaciones. Apple los documenta por separado en su modificador de marco flexible y su modificador de forma de contenido.
Por ejemplo, una fila puede parecer ancha porque su contenedor ocupa toda la ventana, mientras que la interacción está asociada únicamente a una vista de texto corta. Del mismo modo, añadir espacio vacío al diseño no especifica explícitamente que toda la fila deba participar en la detección de pulsaciones. La solución práctica es establecer primero los límites previstos de la fila y luego asignar una forma de interacción a esa región.
Usa .frame(maxWidth: .infinity, alignment: .leading) para permitir que la etiqueta se expanda cuando su elemento principal proponga más ancho. Después, aplica .contentShape(.interaction, Rectangle()) a esa etiqueta expandida. Apple define el tipo de forma de contenido interaction específicamente para la detección de pulsaciones y la accesibilidad.
Esto no hace que la fila tenga un ancho infinito. El marco sigue participando en la propuesta de diseño de su elemento principal. Un elemento principal estrecho produce una fila estrecha, y una forma de contenido no puede aportar el espacio de diseño que falta. Esta combinación satisface dos requisitos: una etiqueta con suficiente ancho y una región explícita para detectar pulsaciones.
Crea un ejemplo completo de botón de fila
Crea en Xcode un proyecto de app para macOS con SwiftUI y Swift. Establece macOS 12 o posterior como destino de implementación para la sobrecarga explícita de forma de contenido .interaction, que Apple indica como disponible a partir de macOS 12. Sustituye la declaración de app y la vista de contenido generadas por el programa de un solo archivo que aparece a continuación; conserva una sola declaración de app @main. Consulta la documentación de Apple sobre la sobrecarga de forma de contenido para ver su disponibilidad.
El ejemplo usa un Button porque cada fila inicia una acción. Apple recomienda usar botones para los elementos de interfaz que realizan acciones y admite explícitamente vistas arbitrarias como etiquetas de botón. Su documentación sobre gestos de pulsación también recomienda personalizar un botón cuando un control basado en gestos sería funcionalmente equivalente.
import SwiftUI
@main
struct ClickableRowsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct SampleItem: Identifiable {
let id: Int
let title: String
let color: Color
}
struct ContentView: View {
private let items = [
SampleItem(id: 1, title: "Blue square", color: .blue),
SampleItem(id: 2, title: "Orange square", color: .orange),
SampleItem(id: 3, title: "Purple square", color: .purple)
]
@State private var lastActivated = "None"
@State private var activationCount = 0
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Choose a shape")
.font(.headline)
ScrollView {
VStack(spacing: 8) {
ForEach(items) { item in
Button {
lastActivated = item.title
activationCount += 1
} label: {
HStack(spacing: 12) {
Rectangle()
.fill(item.color)
.frame(width: 24, height: 24)
Text(item.title)
}
.padding(.horizontal, 12)
.padding(.vertical, 10)
.frame(maxWidth: .infinity,
alignment: .leading)
.contentShape(.interaction, Rectangle())
}
.buttonStyle(.plain)
.background(Color.gray.opacity(0.12))
}
}
}
Text("Last activated: \(lastActivated)")
Text("Activations: \(activationCount)")
}
.padding(16)
.frame(minWidth: 360, minHeight: 280)
}
}
Todos los elementos y el estado de interacción permanecen en memoria. Cada elemento tiene una identidad entera fija, de acuerdo con el protocolo Identifiable de Swift, que representa una identidad estable. La vista principal es propietaria del estado de activación, siguiendo las pautas de Apple para mantener privado el State local en la vista que debe ser su propietaria.
Ejecuta la app y pulsa el área vacía a la derecha de cualquier título. El recuento de activaciones debería aumentar en uno y el estado debería indicar el nombre de esa fila. El fondo gris permite ver el área prevista de la fila durante esta comprobación; la forma de contenido explícita define la región de interacción.
Este programa se revisó comparándolo con la documentación de la API consultada, pero aquí no se compiló ni se ejecutó porque no había una cadena de herramientas de Swift disponible. Comprueba el comportamiento con el puntero y el teclado en tu destino de macOS antes de integrarlo.
¿Dónde deben ir el marco, el relleno y la forma de contenido?
En este ejemplo, el orden es deliberado: crea el contenido de la etiqueta, añade su relleno interno, expande la etiqueta resultante y define la forma de interacción sobre esa región. Todas estas operaciones se realizan dentro del cierre de la etiqueta del botón. Apple documenta que el orden de los modificadores importa.
Al poner el relleno dentro de esta etiqueta con forma, la región rellenada queda incluida en el área prevista de interacción. En cambio, poner el relleno fuera del botón crea espacio alrededor dentro del diseño; no des por hecho que ese espacio pertenece a la etiqueta interactiva del botón. Del mismo modo, expandir solo un contenedor externo no define la misma geometría de etiqueta que expandir el contenido dentro del botón.
El modificador .buttonStyle(.plain) mantiene sencillo el aspecto del ejemplo. Apple describe el estilo de botón plain como sin decoración cuando está inactivo, aunque puede producir efectos en los estados presionado, enfocado o habilitado. Es una elección de estilo, no un sustituto de definir el ancho y la forma de contenido de la etiqueta.
Para un destino de implementación anterior a macOS 12, sustituye .contentShape(.interaction, Rectangle()) por .contentShape(Rectangle()). Apple documenta la sobrecarga anterior como la que define la forma para detectar pulsaciones y señala que está disponible a partir de macOS 10.15. Este cambio modifica el requisito de la API de forma de contenido; comprueba por separado las demás API de tu app.
¿Conviene usar un botón, la selección de una lista o un gesto para una fila?
Elige la interacción según lo que significa pulsar la fila. Hacer que responda el espacio vacío debe conservar ese significado y la propiedad de los controles que haya dentro de la fila.
| Comportamiento para el lector | Opción inicial adecuada | Límite que se debe conservar |
|---|---|---|
| Toda la fila realiza una acción | Un Button con una etiqueta expandida y con forma |
Mantén un único propietario de la acción para la fila |
| La fila representa la selección actual | Un List con un enlace de selección |
Deja que la lista gestione el comportamiento de selección |
| Una vista personalizada necesita un gesto de pulsación independiente | Una vista con forma y onTapGesture |
Asocia el gesto a la región prevista |
| La fila contiene varios controles independientes | Controles hermanos separados | Asigna a cada control su propia área |
Para seleccionar elementos de una lista, usa el modelo de selección del contenedor antes de añadir un controlador de pulsación para toda la fila. La documentación de Apple sobre List describe un enlace a un identificador de elemento opcional para una selección única y un conjunto de identificadores para varias selecciones. Un botón que realiza una acción y una fila de lista que queda seleccionada corresponden a requisitos de interacción distintos.
Un gesto es adecuado cuando necesitas específicamente reconocer gestos en una vista personalizada. Aplica la misma secuencia geométrica —relleno y marco, seguidos de la forma de contenido— antes de añadir onTapGesture. Apple documenta su parámetro count como el número de toques o pulsaciones necesarios. Una forma de contenido proporciona la región objetivo; por sí sola no proporciona una acción. Consulta onTapGesture(count:perform:).
Si la fila también contiene un botón o interruptor independiente, mantenlo fuera de la etiqueta del botón principal. Una estructura útil es una pila horizontal con un botón principal expandible y un control independiente al final. El espacio vacío de la etiqueta principal pertenece a su acción; el control final es propietario de su propia región. Evita anidar controles interactivos dentro de un botón que abarque toda la fila.
No añadas modificadores de prioridad de gestos solo para corregir la detección de pulsaciones en el espacio vacío. Apple documenta highPriorityGesture como un modificador que cambia la precedencia, mientras que simultaneousGesture permite que los gestos se procesen a la vez. Estas operaciones cambian las relaciones entre gestos y pueden afectar a las interacciones de las vistas descendientes; no establecen la geometría de fila que falta.
Diagnostica una fila que aún no detecta algunas pulsaciones
Sigue esta secuencia con la versión más sencilla de tu fila. Te permitirá distinguir un problema de tamaño de uno de forma del área interactiva o de una vista superpuesta.
- Comprueba los límites reales de la etiqueta. Añade temporalmente
.border(Color.orange)justo después del modificador de marco de la etiqueta. Si el borde termina junto al texto, revisa la propuesta de ancho del elemento principal y las restricciones de ancho fijo. - Comprueba la ubicación de los modificadores. Confirma que la forma de contenido cubre la etiqueta expandida, incluido el relleno que quieres que se pueda pulsar.
- Comprueba quién controla la interacción. Usa la acción del botón para una fila con botón. Al aislar el problema, elimina los gestos redundantes de pulsación de la fila.
- Comprueba si hay decoraciones superpuestas. Revisa las superposiciones y las vistas hermanas que aparecen encima de la fila. Para una decoración que nunca debería recibir entradas, considera aplicar
.allowsHitTesting(false)solo a esa decoración. - Vuelve a añadir los demás controles uno por uno. Comprueba el área de cada control al volver a añadirlo y, después, prueba el espacio vacío restante.
Apple documenta allowsHitTesting(_:) como un modificador que controla si una vista participa en las operaciones de detección de pulsaciones. Si se aplica a toda la fila, esta dejará de participar; por tanto, aplícalo a la vista decorativa concreta cuando ese sea el comportamiento previsto.
Haz varias pulsaciones en lugares distintos para confirmar el resultado. La tabla describe el comportamiento esperado del programa proporcionado, no resultados de pruebas medidos.
| Lugar de la pulsación | Resultado esperado |
|---|---|
| Cuadrado de color o título de la fila | Activar esa fila una vez |
| Espacio vacío a la derecha, dentro de la fila gris | Activar esa fila una vez |
| Relleno interno encima o debajo del título | Activar esa fila una vez |
| Separación entre filas | No activar ninguna fila |
| Relleno exterior o área vacía debajo de las filas | No activar ninguna fila |
Cambia el tamaño de la ventana y repite la comprobación del espacio vacío. Comprueba también el enfoque y la activación mediante el teclado con los ajustes de navegación por teclado de tu Mac. Estas comprobaciones ayudan a determinar si el componente final se comporta como el control previsto y no solo si responde al puntero.
¿Cuándo importa la detección de pulsaciones de AppKit?
Para una fila SwiftUI normal, empieza con las API de diseño, forma de contenido y controles de SwiftUI. AppKit cobra relevancia cuando tu app contiene vistas nativas personalizadas o una vista integrada que modifica a qué vista se dirigen los eventos.
Apple documenta NSView.hitTest(_:) como una función que busca el descendiente más lejano que contenga un punto. Una ventana la usa principalmente para determinar qué vista recibe un evento de pulsación del ratón. El punto de entrada se expresa en el sistema de coordenadas de la supervista, y Apple indica que rara vez es necesario invocarla directamente.
Si mantienes una vista AppKit personalizada, investiga sus límites y su implementación de detección de pulsaciones en ese punto de la jerarquía. Una forma de contenido de SwiftUI no ordena eludir la propiedad de los eventos de una vista nativa independiente. Aplica la corrección en la jerarquía de vistas que recibe la pulsación.
Para la fila de este tutorial, la solución admitida es sencilla: permite que la etiqueta del botón ocupe el ancho previsto de la fila, define su forma rectangular de interacción y mantén los demás controles en sus propias regiones. Así, el espacio vacío útil se puede pulsar y las demás interacciones de la ventana siguen funcionando.
Fuentes
- contentShape(_:eoFill:) — Apple
- contentShape(::eoFill:) — Apple
- interaction — Apple
- frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:) — Apple
- Button — Apple
- plain — Apple
- padding(::) — Apple
- Identifiable — Apple
- State — Apple
- List — Apple
- onTapGesture(count:perform:) — Apple
- highPriorityGesture(_:including:) — Apple
- simultaneousGesture(_:including:) — Apple
- allowsHitTesting(_:) — Apple
- hitTest(_:) — Apple