Como iOS Developer, sabes que las hojas modales (o sheets) son uno de los componentes de interfaz de usuario más utilizados en todo el ecosistema de Apple. Son perfectas para presentar flujos de creación, ajustes, detalles secundarios o formularios de inicio de sesión. Sin embargo, aunque el gesto de deslizar para descartar (swipe-to-dismiss) es intuitivo en los iPhone modernos, depender únicamente de él puede ser un grave error de usabilidad.
En este tutorial exhaustivo, vamos a explorar a fondo cómo añadir un botón de cerrar en una sheet en SwiftUI. A través de la programación Swift, aprenderemos no solo a implementar la funcionalidad básica, sino a crear componentes robustos, accesibles y multiplataforma que funcionen a la perfección en iOS, macOS y watchOS utilizando Xcode. Prepárate para dominar la navegación modal en SwiftUI.
1. ¿Por qué es crucial añadir un botón de cerrar en una sheet en SwiftUI?
Antes de abrir Xcode y empezar a teclear código en Swift, es importante entender la justificación de diseño (UX) detrás de esta práctica. ¿Por qué molestarnos en añadir un botón de cerrar en una sheet en SwiftUI si Apple ya proporciona un gesto nativo para cerrarla?
- Accesibilidad universal: Muchos usuarios tienen dificultades motoras que les impiden realizar gestos precisos de deslizamiento. Un botón de cerrar explícito garantiza que todos puedan navegar por tu aplicación.
- El paradigma de macOS: Si estás construyendo una aplicación multiplataforma, debes saber que en macOS los usuarios no pueden “deslizar” una ventana con el ratón. Un botón de “Cerrar” o “Cancelar” es obligatorio.
- Prevenir cierres accidentales: En formularios donde el usuario introduce datos, a menudo deshabilitamos el deslizamiento interactivo (usando
.interactiveDismissDisabled()) para evitar la pérdida de información. En estos casos, un botón de cierre es la única ruta de escape. - Claridad visual: Un botón en la esquina superior derecha (o izquierda) es un patrón universalmente reconocido que reduce la carga cognitiva del usuario.
2. La Evolución: De presentationMode a dismiss
Si llevas un tiempo en la programación Swift, quizás recuerdes que en las primeras versiones de SwiftUI (iOS 13 y 14), para cerrar una vista programáticamente usábamos @Environment(\.presentationMode). Aunque sigue funcionando, Apple lo considera obsoleto para esta tarea específica.
Desde iOS 15 en adelante, la forma moderna, limpia y recomendada de cerrar cualquier vista modal es utilizando el entorno dismiss. Es mucho más directo y requiere menos código.
Veamos cómo se configura la vista principal que invocará nuestra sheet:
import SwiftUI
struct ContentView: View {
// 1. Estado que controla la visibilidad de la sheet
@State private var showSettingsSheet = false
var body: some View {
VStack {
Image(systemName: "gearshape.fill")
.imageScale(.large)
.foregroundColor(.blue)
.padding()
Text("Panel de Control")
.font(.title)
.padding(.bottom, 20)
Button(action: {
showSettingsSheet = true
}) {
Text("Abrir Configuración")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color.blue)
.cornerRadius(10)
}
.padding(.horizontal)
}
// 2. Presentación de la Sheet
.sheet(isPresented: $showSettingsSheet) {
SettingsSheetView()
}
}
}
3. Implementación Estándar: Usando NavigationView y Toolbar
La forma más “nativa” y respetuosa con las Guías de Interfaz Humana (HIG) de Apple para añadir un botón de cerrar en una sheet en SwiftUI es envolver el contenido de tu modal en un NavigationView (o NavigationStack en iOS 16+) y colocar el botón en la barra de herramientas (Toolbar).
Vamos a construir nuestra SettingsSheetView utilizando este enfoque:
import SwiftUI
struct SettingsSheetView: View {
// 1. Declaramos la variable de entorno para descartar la vista
@Environment(\.dismiss) var dismiss
var body: some View {
// En iOS 16+, prefiere NavigationStack. Usamos NavigationView por retrocompatibilidad.
NavigationView {
Form {
Section(header: Text("Perfil")) {
Text("Editar nombre")
Text("Cambiar avatar")
}
Section(header: Text("Preferencias")) {
Toggle("Notificaciones Push", isOn: .constant(true))
Toggle("Modo Oscuro", isOn: .constant(false))
}
}
.navigationTitle("Configuración")
.navigationBarTitleDisplayMode(.inline)
// 2. Añadimos la barra de herramientas
.toolbar {
// 3. Colocamos el botón en la posición 'cancellationAction'
ToolbarItem(placement: .cancellationAction) {
Button(action: {
dismiss()
}) {
// Podemos usar texto o un icono (SF Symbol)
Text("Cerrar")
.fontWeight(.semibold)
}
// Mejora de accesibilidad
.accessibilityLabel("Cerrar pantalla de configuración")
}
}
}
}
}
¿Por qué usar .cancellationAction?
Como iOS Developer, debes aprovechar la semántica de SwiftUI. Al colocar el ToolbarItem con el placement .cancellationAction, el sistema operativo decide automáticamente la mejor ubicación para el botón. En iOS, normalmente lo colocará en la esquina superior izquierda. Si usamos .confirmationAction (por ejemplo, para un botón “Guardar”), lo colocará en la derecha. Esto asegura que tu aplicación se sienta consistente con el resto del sistema.
4. Construyendo un Botón de Cerrar Flotante Personalizado (Estilo “X”)
A veces, el diseño de tu aplicación no requiere una barra de navegación completa. Quizás estás mostrando una imagen a pantalla completa, una tarjeta de presentación, o un modal muy visual donde un NavigationStack rompería la estética. En estos casos, la programación Swift nos permite usar un ZStack para superponer un botón flotante.
Aquí te muestro cómo añadir un botón de cerrar en una sheet en SwiftUI con un estilo flotante personalizado:
import SwiftUI
struct CustomFloatingSheetView: View {
@Environment(\.dismiss) var dismiss
var body: some View {
ZStack(alignment: .topTrailing) {
// 1. El contenido principal de la hoja
Color.indigo.opacity(0.1)
.ignoresSafeArea()
VStack(spacing: 20) {
Image(systemName: "star.fill")
.font(.system(size: 60))
.foregroundColor(.yellow)
Text("¡Felicidades!")
.font(.largeTitle)
.bold()
Text("Has desbloqueado un nuevo nivel.")
.font(.body)
.foregroundColor(.secondary)
}
// Aseguramos que el contenido esté centrado
.frame(maxWidth: .infinity, maxHeight: .infinity)
// 2. El Botón de Cerrar Flotante
Button(action: {
dismiss()
}) {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 30))
.foregroundColor(.gray.opacity(0.8))
.symbolRenderingMode(.hierarchical)
}
.padding() // Margen desde los bordes seguros
.accessibilityLabel("Cerrar")
.accessibilityHint("Cierra esta ventana emergente y vuelve a la pantalla anterior.")
}
}
}
Análisis de la arquitectura visual
Al utilizar un ZStack(alignment: .topTrailing), le estamos diciendo a SwiftUI que todos los elementos se apilen uno encima del otro, y que por defecto se alineen en la esquina superior derecha. Esto es increíblemente útil porque posiciona nuestro botón “X” exactamente donde los usuarios esperan encontrarlo, sin cálculos matemáticos complejos de padding.
5. El Desafío Multiplataforma: iOS vs. macOS vs. watchOS
La verdadera magia de la programación Swift actual reside en su capacidad multiplataforma. Sin embargo, un buen iOS Developer sabe que compartir código en Xcode no significa ignorar las convenciones de cada plataforma.
Veamos cómo se comporta el concepto de añadir un botón de cerrar en una sheet en SwiftUI en diferentes sistemas operativos:
- iOS/iPadOS: Las sheets se deslizan desde abajo. El usuario espera botones de cierre en la parte superior o deslizar hacia abajo.
- macOS: Las sheets aparecen como paneles modales anclados a la parte superior de la ventana principal. No se pueden deslizar. Un botón de “Cancelar” o “Aceptar/Cerrar” en la esquina inferior derecha es el estándar histórico, aunque las barras de herramientas superiores también son aceptables hoy en día.
- watchOS: El espacio es diminuto. A menudo, SwiftUI añade automáticamente un botón de cancelar en la esquina superior izquierda si detecta un modal. Añadir otro manualmente puede resultar en botones duplicados.
Escribiendo un Componente de Cierre Multiplataforma Inteligente
Para crear una base de código verdaderamente profesional, podemos crear un componente reutilizable que renderice el botón correcto según el sistema operativo utilizando directivas de compilación (#if os(...)).
import SwiftUI
// Creamos un ViewModifier personalizado
struct AdaptiveCloseButtonModifier: ViewModifier {
@Environment(\.dismiss) var dismiss
func body(content: Content) -> some View {
#if os(iOS)
// En iOS usamos NavigationStack y Toolbar para una experiencia nativa
NavigationStack {
content
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button { dismiss() } label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.secondary)
}
}
}
}
#elseif os(macOS)
// En macOS, añadimos el botón en la parte inferior derecha
VStack(spacing: 0) {
content
Divider()
HStack {
Spacer()
Button("Cerrar") {
dismiss()
}
.keyboardShortcut(.defaultAction) // Permite cerrar con la tecla Enter
.padding()
}
.background(Color(NSColor.windowBackgroundColor))
}
.frame(minWidth: 400, minHeight: 300) // Mac requiere dimensiones explícitas a menudo
#elseif os(watchOS)
// En watchOS solemos dejar que el sistema maneje el cierre nativo,
// o colocamos un botón muy simple al final del contenido.
ScrollView {
VStack {
content
Button("Cerrar", role: .cancel) {
dismiss()
}
.padding(.top)
}
}
#endif
}
}
// Extensión para que sea fácil de aplicar en cualquier vista
extension View {
func withAdaptiveCloseButton() -> some View {
self.modifier(AdaptiveCloseButtonModifier())
}
}
¿Cómo utilizar esta maravilla de la programación Swift? Es facilísimo. Simplemente diseña tu vista modal enfocándote solo en el contenido y añádele nuestro modificador al final:
struct MyCrossPlatformSheet: View {
var body: some View {
VStack {
Text("Contenido Importante")
.font(.title)
Text("Esta vista se adapta perfectamente a iPhone, Mac y Apple Watch.")
.multilineTextAlignment(.center)
.padding()
}
// Magia multiplataforma aplicada aquí:
.withAdaptiveCloseButton()
}
}
6. Mejores Prácticas y Prevención de Errores
Para concluir esta guía, como iOS Developer, aquí tienes un resumen de las mejores prácticas que debes tener en cuenta al trabajar con sheets en Xcode:
1. Cuidado con el Estado de los Datos
Cuando un usuario cierra una sheet (ya sea deslizando o pulsando el botón), asegúrate de que los datos no guardados se manejen correctamente. Si estás editando un formulario, puedes interceptar el cierre usando .interactiveDismissDisabled(hasUnsavedChanges) y mostrar una alerta de confirmación (“¿Seguro que quieres descartar los cambios?”).
2. Accesibilidad ante todo
Como vimos en los ejemplos anteriores, siempre que uses un icono de SF Symbols para tu botón de cerrar (como la típica “X”), debes proveer un .accessibilityLabel("Cerrar"). De lo contrario, VoiceOver leerá algo confuso como “Cruz en círculo rellenado”, lo cual frustrará a los usuarios con discapacidad visual.
3. No abuses de los modales
Aunque es fácil abrir sheets en SwiftUI, las Guías de Interfaz Humana recomiendan usarlas con moderación. Si un flujo requiere múltiples pasos, es mejor usar la navegación convencional (NavigationLink) en lugar de apilar sheets una sobre otra, lo cual crea una arquitectura confusa y propensa a bugs visuales.
Conclusión
Saber cómo añadir un botón de cerrar en una sheet en SwiftUI de forma correcta es mucho más que simplemente dibujar una “X” en la pantalla. Implica comprender el flujo de navegación, la inyección de dependencias mediante el @Environment, y las diferencias fundamentales de usabilidad entre los dispositivos de Apple.
Si tienes cualquier duda sobre este artículo, contacta conmigo y estaré encantado de ayudarte . Puedes contactar conmigo en mi perfil de X o en mi perfil de Instagram.










