Para una lista de elementos en Mac, vincula la selección a una List de SwiftUI y proporciona una acción principal mediante su menú contextual que tiene en cuenta la selección. Un clic selecciona la fila; un doble clic la abre. Mantén el estado de selección separado del estado de presentación.
Este tutorial implementa esa interacción en tu propia app para macOS con tres elementos sintéticos en memoria. También explica los límites de las superficies personalizadas de SwiftUI y las vistas de AppKit para que puedas elegir un enfoque sin hacer que varias capas compitan por el mismo clic.
Usa la acción principal del contenedor seleccionable
Apple documenta explícitamente la selección con un clic y las acciones principales con doble clic para los contenedores seleccionables en macOS. El punto de entrada compatible de SwiftUI es contextMenu(forSelectionType:menu:primaryAction:), disponible a partir de macOS 13. Añádelo a un contenedor que admita selección, como List o Table. Documentación de Apple sobre menús contextuales que tienen en cuenta la selección.
Hay tres responsabilidades distintas: el modelo identifica cada elemento, la lista mantiene la selección mediante un enlace y tu controlador de la acción principal decide qué significa abrir. En este ejemplo, abrir muestra una hoja con los detalles del elemento. Seleccionar un elemento solo cambia la fila resaltada y la etiqueta de selección.
Elige la implementación según la vista que gestione la interacción:
| Tu interfaz | Punto de partida adecuado | Responsabilidad que conservas |
|---|---|---|
| Una lista o tabla de elementos de SwiftUI en macOS 13 o posterior | Enlace de selección más acción principal que tiene en cuenta la selección | Resolver los identificadores de acción y presentar el contenido correspondiente |
| Una celda o forma personalizada de SwiftUI | Composición explícita de gestos | Apariencia de selección, política de activación y otras vías de entrada |
| Una tabla de AppKit integrada en SwiftUI | Selección de NSTableView y doubleAction |
Conectar la selección y la activación con el estado de SwiftUI |
| Una superficie de dibujo personalizada de AppKit | Gestión propia de eventos del ratón | Detección de objetivos, identidad del elemento y política de secuencias de clics |
La primera opción se ajusta directamente a la tarea de este artículo. Las opciones personalizadas son útiles cuando la interfaz necesita realmente una superficie diferente; también requieren implementar más comportamientos de interacción. Apple documenta las alternativas subyacentes en composición de gestos, acciones de doble clic de tablas de AppKit y gestión de mouse-down.
Crea un ejemplo completo de selección única
Crea en Xcode un proyecto de app para macOS con una interfaz de SwiftUI y establece el destino de implementación en macOS 13 o posterior. Sustituye las declaraciones generadas de la app y la vista de contenido por el siguiente archivo único. Mantén una sola declaración de app @main en el destino.
El programa usa identificadores enteros fijos, tres círculos de colores y estado local. No lee archivos ni abre recursos externos. El botón Open, con estilo de barra de herramientas, y el comando contextual Open llaman a la misma función que la activación con doble clic.
import SwiftUI
struct SampleItem: Identifiable {
let id: Int
let name: String
let tint: Color
}
@main
struct ClickSelectionDemoApp: App {
var body: some Scene {
WindowGroup {
ItemBrowser()
}
}
}
struct ItemBrowser: View {
private let items = [
SampleItem(id: 1, name: "Circle A", tint: .blue),
SampleItem(id: 2, name: "Circle B", tint: .orange),
SampleItem(id: 3, name: "Circle C", tint: .purple)
]
@State private var selectedID: SampleItem.ID? = nil
@State private var openedItem: SampleItem? = nil
@State private var message = "Select an item."
private var selectedItem: SampleItem? {
items.first { $0.id == selectedID }
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text(selectedItem.map {
"Selected: \($0.name)"
} ?? "No selection")
Spacer()
Button("Open") {
if let id = selectedID {
openItems([id])
}
}
.disabled(selectedItem == nil)
}
List(items, selection: $selectedID) { item in
HStack(spacing: 10) {
Circle()
.fill(item.tint)
.frame(width: 18, height: 18)
Text(item.name)
}
.padding(.vertical, 4)
}
.contextMenu(forSelectionType: SampleItem.ID.self) { ids in
if ids.count == 1 {
Button("Open") {
openItems(ids)
}
}
} primaryAction: { ids in
openItems(ids)
}
Text(message)
.font(.caption)
}
.padding()
.frame(minWidth: 360, minHeight: 280)
.sheet(item: $openedItem) { item in
VStack(spacing: 16) {
Circle()
.fill(item.tint)
.frame(width: 80, height: 80)
Text(item.name)
.font(.title2)
Button("Close") {
openedItem = nil
}
}
.padding(32)
.frame(minWidth: 260, minHeight: 220)
}
}
private func openItems(_ ids: Set<SampleItem.ID>) {
guard ids.count == 1, let id = ids.first else {
message = "Choose exactly one item to open."
return
}
guard let item = items.first(where: { $0.id == id }) else {
message = "That item is no longer available."
return
}
openedItem = item
message = "Opened: \(item.name)"
}
}
Se han inspeccionado las declaraciones y referencias, pero este ejemplo no se ha compilado ni ejecutado aquí: el entorno disponible no tiene herramientas de Swift ni una interfaz gráfica de macOS. Compílalo en Xcode y realiza las comprobaciones de interacción indicadas a continuación.
Mantén diferenciadas la identidad, la selección y la apertura
SampleItem cumple con el protocolo Identifiable de Swift. Su id fijo representa el mismo elemento en las actualizaciones de la vista. Apple define este protocolo en torno a una identidad estable; el nombre que se muestra es una propiedad separada. En una app más grande, conserva el identificador de un elemento cuando cambien su nombre o apariencia. Documentación de Apple sobre Identifiable.
El selectedID opcional crea una lista de selección única. En cambio, un conjunto de identificadores permitiría la selección múltiple. La documentación de listas de SwiftUI describe estas dos formas de enlace y la apariencia de selección que proporciona el contenedor. Documentación de Apple sobre List.
openedItem controla la presentación por separado. Al asignarle un elemento, se muestra la hoja; al asignarle nil, se cierra. Esto sigue la API documentada de hojas basadas en elementos. Como al seleccionar una fila nunca se asigna openedItem, la selección por sí sola no puede activar la hoja en esta implementación. Documentación de Apple sobre hojas basadas en elementos.
Resuelve los identificadores que recibe la acción
La API del menú contextual toma el tipo de identificador del contenedor y su acción recibe un conjunto de identificadores. Por eso, el ejemplo usa SampleItem.ID.self, que coincide con la selección de la lista. Resuelve el argumento de la devolución de llamada en vez de sustituirlo por un valor de selección leído por separado. Documentación de Apple sobre los parámetros del menú contextual.
El controlador acepta deliberadamente exactamente un identificador. Un conjunto vacío muestra un mensaje y un identificador que no se puede resolver muestra otro. Si más adelante habilitas la selección múltiple, define la política de apertura antes de cambiar esta condición: muestra una vista de detalles del grupo, abre ventanas separadas o exige una sola selección. Elegir arbitrariamente el primer elemento ocultaría una política indefinida.
Por qué dos controladores de toque requieren una decisión de diseño distinta
Una celda personalizada puede necesitar sus propios gestos. onTapGesture(count:perform:) de SwiftUI acepta un número requerido de clics o toques. Sin embargo, su documentación no establece que colocar dos modificadores independientes en una vista cree un contrato exclusivo para distinguir entre un clic simple y uno doble. Documentación de Apple sobre acciones al tocar.
Primero decide qué significa la selección durante un doble clic. En un explorador de elementos convencional, seleccionar el elemento antes de abrirlo resulta útil y coherente con la solución basada en contenedores. Un requisito distinto —ejecutar la acción del clic simple solo si la secuencia no termina siendo un doble clic— requiere arbitraje.
Para una superficie personalizada, SwiftUI proporciona exclusively(before:): solo uno de los gestos componentes tiene éxito y se da prioridad al primero. El siguiente fragmento expresa un gesto; no es un reemplazo completo del ejemplo de la lista: Documentación de Apple sobre gestos exclusivos.
TapGesture(count: 2)
.exclusively(before: TapGesture(count: 1))
Esto da prioridad al reconocimiento del doble clic. Añade el gesto compuesto al elemento personalizado y gestiona el resultado según tu política de selección y apertura. TapGesture documenta el reconocimiento según la cantidad de toques requerida. Documentación de Apple sobre TapGesture.
Esa política tiene una consecuencia temporal: para decidir que una secuencia es exclusivamente un clic simple, hay que descartar que sea un doble clic. Esto se deriva del requisito de interacción, no de un retraso fijo de SwiftUI documentado. No prometas que el clic simple se completará de inmediato mientras también exiges que nunca ocurra durante una secuencia de doble clic.
En una celda personalizada, define también el área en la que se puede hacer clic. contentShape(Rectangle()) puede hacer que una región rectangular con relleno participe en la detección de objetivos; no establece la prioridad entre gestos. Mantén separados estos dos aspectos al diagnosticar clics que no se registran. Documentación de Apple sobre formas de contenido.
Conserva la gestión de eventos cuando intervenga AppKit
Si una tabla de AppKit existente gestiona las filas, usa el mecanismo de activación compatible de esa tabla. NSTableView.doubleAction envía un selector a su destino al hacer doble clic. Apple también documenta clickedRow y clickedColumn para determinar dónde ocurrió la activación, incluidos los encabezados de columna. Comprueba que la ubicación identifique un elemento antes de abrirlo. Documentación de Apple sobre doubleAction.
En una superficie personalizada de AppKit, mouseDown(with:) recibe un evento de pulsación del botón izquierdo, y NSEvent.clickCount informa del número de clics asociado a los eventos de mouse-down o mouse-up. Leer esa propiedad en un evento que no corresponda queda fuera del contrato documentado y puede provocar una excepción. Mantén esa interpretación dentro de la vista que gestiona el evento del ratón correspondiente. Documentación de Apple sobre mouse-down, documentación de Apple sobre el recuento de clics.
Usa NSViewRepresentable al integrar esa vista de AppKit en SwiftUI. Créala en makeNSView, sincroniza la configuración actual en updateNSView y usa un coordinador cuando la comunicación con el destino y la acción o con un delegado requiera un puente. Apple documenta la creación, actualización, coordinación y desmontaje como partes de esta integración. Documentación de Apple sobre NSViewRepresentable.
Un elemento superpuesto que recibe eventos del ratón puede cambiar qué vista gestiona el clic. AppKit utiliza la detección de objetivos para determinar el destinatario de los eventos de mouse-down, así que un elemento superpuesto que captura eventos no debe tratarse como un observador invisible. Para esta tarea, mantén el reconocimiento en el contenedor seleccionable o en la superficie personalizada que realmente gestione la interacción con el elemento. Documentación de Apple sobre la detección de objetivos.
Comprueba la selección y la apertura como resultados separados
Sigue esta secuencia después de compilar el ejemplo:
- Haz clic una vez en
Circle A. Su fila debe quedar seleccionada, la etiqueta de selección debe mostrar su nombre y no debe aparecer ninguna hoja. - Haz doble clic en
Circle B. Comprueba que la hoja de detalles muestreCircle B. - Cierra la hoja y haz clic una vez en
Circle C. Comprueba que la selección cambie sin abrir otra hoja. - Activa el botón visible
Open. Comprueba que abra el elemento seleccionado mediante el mismo controlador. - Invoca el comando contextual
Opende una fila y comprueba qué elemento abre. - Haz clic y doble clic en un espacio vacío de la lista. Comprueba que no abra un elemento que no corresponda.
- Si añades controles integrados, arrastre o elementos superpuestos, repite las comprobaciones exactamente sobre esas regiones.
Si la selección abre detalles de forma inesperada, comprueba dónde se asigna el estado de presentación. Si la activación abre el elemento equivocado, comprueba los identificadores de la devolución de llamada y la búsqueda en el modelo. Si no se registran los clics en parte de una celda personalizada, revisa su diseño y forma de detección de objetivos antes de cambiar la prioridad entre gestos. Estas comprobaciones aíslan el estado, el destino de la acción y la entrega de eventos en vez de cambiarlos todos a la vez.
Para una lista de elementos convencional en Mac, basa la implementación en el enlace de selección y la acción principal del contenedor. Añade reconocimiento personalizado solo cuando la superficie o la interacción lo requiera, y haz que cada vía de apertura resuelva el elemento previsto mediante un único controlador explícito.
Fuentes
- contextMenu(forSelectionType:menu:primaryAction:) — Apple
- List — Apple
- Identifiable — Apple
- sheet(item:onDismiss:content:) — Apple
- onTapGesture(count:perform:) — Apple
- exclusively(before:) — Apple
- TapGesture — Apple
- contentShape(_:eoFill:) — Apple
- doubleAction — Apple
- mouseDown(with:) — Apple
- clickCount — Apple
- NSViewRepresentable — Apple
- hitTest(_:) — Apple