Como iOS Developer, sabes que la experiencia de usuario (UX) lo es todo. Las aplicaciones más exitosas en el ecosistema de Apple no solo funcionan bien, sino que se sienten naturales, fluidas e intuitivas. Uno de los gestos más arraigados en la memoria muscular de los usuarios de iPhone y iPad es el de deslizar hacia abajo para cerrar una pantalla o modal.
En este tutorial vamos a sumergirnos en el mundo de la programación Swift para aprender cómo implementar un swipe to dismiss en SwiftUI. Exploraremos desde las soluciones nativas más sencillas hasta la creación de un sistema de gestos totalmente personalizado mediante DragGesture, asegurándonos de que nuestro código en Swift sea elegante, escalable y funcione en proyectos multiplataforma (iOS, macOS y watchOS) utilizando Xcode.
1. El Paradigma del “Swipe to Dismiss” en el Ecosistema Apple
Antes de escribir una sola línea de código en Xcode, es crucial entender cómo y cuándo aplicar este patrón de diseño.
En iOS, el swipe to dismiss en SwiftUI es el estándar de oro para las hojas modales (sheets). Cuando un usuario abre un detalle, una configuración o un formulario rápido, espera poder cerrarlo arrastrándolo hacia la parte inferior de la pantalla. En watchOS, los deslizamientos son fundamentales debido al tamaño reducido de la pantalla. Sin embargo, en macOS, el paradigma cambia drásticamente: los usuarios de Mac interactúan con un cursor (o trackpad) y esperan botones de cierre (X) o el uso de la tecla Escape.
Como un iOS Developer moderno, tu objetivo al usar SwiftUI es escribir código que se adapte inteligentemente a cada plataforma sin duplicar esfuerzos.
2. La Solución Nativa: Usando .sheet en SwiftUI
Si estás desarrollando una aplicación estándar y necesitas presentar una vista que el usuario pueda descartar deslizando, SwiftUI hace el trabajo pesado por ti. Desde sus primeras versiones, el modificador .sheet incluye este comportamiento de forma predeterminada en iOS.
Abre Xcode, crea un nuevo proyecto multiplataforma y echa un vistazo a esta implementación básica:
import SwiftUI
struct NativeSwipeToDismissView: View {
@State private var showSheet = false
var body: some View {
VStack {
Button("Mostrar Modal Nativo") {
showSheet = true
}
.buttonStyle(.borderedProminent)
.padding()
}
.sheet(isPresented: $showSheet) {
DetailView()
// A partir de iOS 16, podemos añadir un indicador visual
.presentationDragIndicator(.visible)
}
}
}
struct DetailView: View {
@Environment(\.dismiss) var dismiss
var body: some View {
NavigationView {
ZStack {
Color.blue.opacity(0.1).ignoresSafeArea()
Text("Desliza hacia abajo para cerrar")
.font(.headline)
}
.navigationTitle("Detalle")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Cerrar") {
dismiss()
}
}
}
}
}
}
Ventajas del método nativo:
- Cero esfuerzo: El gesto de deslizar funciona perfectamente fuera de la caja en iOS.
- Accesibilidad: Soporta VoiceOver y otros servicios de accesibilidad de Apple automáticamente.
- Integración: El modificador
.presentationDragIndicator(.visible)(introducido en actualizaciones recientes de Swift) añade la pequeña “píldora” superior que indica al usuario que la vista es arrastrable.
3. El Reto: Vistas Personalizadas y Full Screen Covers
El problema para un iOS Developer surge cuando el diseño exige algo que el .sheet nativo no puede proporcionar. Por ejemplo:
- Un popup flotante en el centro de la pantalla.
- Un
.fullScreenCoverque, por defecto, no incluye el gesto de deslizar para cerrar. - Transiciones visuales altamente personalizadas.
Aquí es donde debemos aplicar la programación Swift avanzada y construir nuestro propio swipe to dismiss en SwiftUI utilizando un DragGesture.
4. Construyendo un Swipe to Dismiss Personalizado con DragGesture
Para lograr un arrastre fluido, necesitamos seguir tres pasos lógicos:
- Rastrear cuánto ha desplazado el usuario su dedo por la pantalla (
offset). - Mover la vista en la pantalla en tiempo real según ese desplazamiento.
- Decidir, al soltar el dedo, si el desplazamiento fue suficiente para cerrar la vista o si la vista debe regresar a su posición original (efecto rebote).
Vamos a crear una vista modal personalizada:
import SwiftUI
struct CustomSwipeToDismissView: View {
@State private var showCustomModal = false
var body: some View {
ZStack {
// Contenido Principal
VStack {
Button("Mostrar Modal Personalizado") {
withAnimation(.spring()) {
showCustomModal = true
}
}
.buttonStyle(.borderedProminent)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
// Capa del Modal Personalizado
if showCustomModal {
Color.black.opacity(0.4)
.ignoresSafeArea()
.onTapGesture {
closeModal()
}
CustomModalContent(isPresented: $showCustomModal)
.transition(.move(edge: .bottom))
}
}
}
private func closeModal() {
withAnimation(.spring()) {
showCustomModal = false
}
}
}
La Lógica del Gesto de Arrastre
Ahora, vamos a implementar CustomModalContent donde ocurre la magia del swipe to dismiss en SwiftUI.
struct CustomModalContent: View {
@Binding var isPresented: Bool
// 1. Estado para almacenar el desplazamiento actual
@State private var dragOffset: CGSize = .zero
var body: some View {
VStack {
Capsule()
.frame(width: 40, height: 6)
.foregroundColor(.gray.opacity(0.5))
.padding(.top, 10)
Spacer()
Text("¡Este es un modal personalizado!")
.font(.title2)
.bold()
.multilineTextAlignment(.center)
Spacer()
Button("Cerrar") {
dismiss()
}
.buttonStyle(.bordered)
.padding(.bottom, 20)
}
.frame(maxWidth: .infinity)
.frame(height: 300)
.background(
RoundedRectangle(cornerRadius: 30)
.fill(Color(UIColor.systemBackground))
.shadow(radius: 20)
)
.padding(.horizontal)
// 2. Aplicamos el desplazamiento a la vista
.offset(y: dragOffset.height > 0 ? dragOffset.height : 0)
// 3. Añadimos el gesto
.gesture(
DragGesture()
.onChanged { value in
// Actualizamos el desplazamiento solo si es hacia abajo
if value.translation.height > 0 {
dragOffset = value.translation
}
}
.onEnded { value in
// 4. Lógica de decisión al soltar
let threshold: CGFloat = 100 // Píxeles necesarios para cerrar
if value.translation.height > threshold || value.velocity.height > 500 {
// Si pasó el umbral o se deslizó muy rápido, cerramos
dismiss()
} else {
// Si no, regresamos a la posición original con una animación
withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
dragOffset = .zero
}
}
}
)
}
private func dismiss() {
withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) {
isPresented = false
dragOffset = .zero
}
}
}
Análisis del Código
offset(y: dragOffset.height > 0 ? dragOffset.height : 0): Esta línea de programación Swift asegura que la vista solo se pueda arrastrar hacia abajo. Si el usuario intenta arrastrar hacia arriba (valores negativos), la vista se mantiene fija.value.velocity.height: Un buen iOS Developer sabe que a veces los usuarios no arrastran hasta el fondo, sino que dan un “toque rápido” (flick) hacia abajo. Leer la velocidad del gesto nos permite cerrar la vista si el impulso es fuerte, mejorando drásticamente la UX..spring(): En SwiftUI, las animaciones de tipo resorte son esenciales para replicar la física real. Cuando la vista no supera el umbral, regresa a su sitio rebotando ligeramente.
5. Adaptación Multiplataforma (iOS, macOS, watchOS)
La promesa de SwiftUI es “Aprende una vez, aplícalo en todas partes”. Sin embargo, compilar este código directamente para macOS en Xcode presentará problemas de usabilidad. En un Mac, arrastrar un panel modal hacia abajo con el ratón no es una interacción estándar.
Para hacer que nuestro código sea verdaderamente profesional, usaremos directivas del compilador de Swift para adaptar el comportamiento a cada sistema operativo.
Vamos a crear un modificador de vista personalizado para encapsular esta lógica, de modo que podamos reutilizarlo en todo nuestro proyecto de Xcode.
import SwiftUI
// Creamos un modificador de vista reutilizable
struct SwipeToDismissModifier: ViewModifier {
var onDismiss: () -> Void
@State private var dragOffset: CGSize = .zero
func body(content: Content) -> some View {
#if os(iOS) || os(watchOS)
// Implementación de gestos para pantallas táctiles
content
.offset(y: dragOffset.height > 0 ? dragOffset.height : 0)
.gesture(
DragGesture()
.onChanged { value in
if value.translation.height > 0 {
dragOffset = value.translation
}
}
.onEnded { value in
if value.translation.height > 100 || value.velocity.height > 300 {
onDismiss()
} else {
withAnimation(.spring()) {
dragOffset = .zero
}
}
}
)
#else
// Implementación para macOS (sin arrastre, el cierre depende de botones)
content
#endif
}
}
// Extensión para facilitar su uso
extension View {
func customSwipeToDismiss(onDismiss: @escaping () -> Void) -> some View {
self.modifier(SwipeToDismissModifier(onDismiss: onDismiss))
}
}
¿Cómo aplicar esto ahora?
Gracias a esta refactorización mediante programación Swift avanzada, aplicar tu swipe to dismiss en SwiftUI a cualquier vista es tan sencillo como añadir una línea de código:
// En tu vista modal:
.customSwipeToDismiss {
withAnimation(.spring()) {
isPresented = false
}
}
En iOS y watchOS, la vista ganará automáticamente la capacidad de ser arrastrada hacia abajo. En macOS, el modificador devolverá el contenido intacto, evitando interacciones extrañas con el ratón, y forzando al usuario a utilizar el botón “Cerrar” (que deberías proveer en la interfaz).
6. Consideraciones de Rendimiento y Buenas Prácticas
Como iOS Developer, al trabajar con gestos continuos (onChanged) en Xcode, debes tener cuidado de no realizar cálculos pesados durante el arrastre. SwiftUI redibuja la vista en cada fotograma del movimiento.
- Evita estados complejos: Asegúrate de que las variables que mutan durante el
DragGesture(comodragOffset) afecten únicamente a modificadores de diseño (como.offseto.opacity) y no disparen redibujados de listas inmensas o llamadas a red. - Usa fondos interactivos: En nuestro ejemplo, el espacio transparente alrededor del modal también escucha toques (
onTapGesture) para cerrar la vista. Esto es una convención estándar; los usuarios esperan que tocar “fuera” del modal lo cierre. - Control de la opacidad: Un toque extra de calidad es hacer que el fondo oscuro (
Color.black.opacity(...)) se vuelva más transparente a medida que el usuario arrastra la vista hacia abajo, vinculando la opacidad aldragOffset.height.
Conclusión
Implementar un swipe to dismiss en SwiftUI de forma personalizada es un rito de paso para cualquier iOS Developer. Demuestra que no solo sabes utilizar los componentes predeterminados de Xcode, sino que comprendes las matemáticas espaciales, las animaciones y el manejo de estados necesarios para manipular la UI a tu antojo usando programación Swift.
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.










