Programación en Swift y SwiftUI para iOS Developers

Cómo navegar de una vista a otra en SwiftUI

Introducción: El Arte de Moverse

Una aplicación de una sola pantalla es como una casa con una sola habitación: funcional para un estudio, pero terrible para una mansión. En el desarrollo de software, la capacidad de mover al usuario de A a B —de una lista de productos al detalle de uno, o de un formulario de registro al “Dashboard”— es lo que da vida y estructura a tu app.

En los primeros días de SwiftUI (2019-2021), la navegación era un punto de dolor constante. NavigationView era limitado, impredecible y difícil de controlar programáticamente. Si venías de UIKit y UINavigationController, sentías que estabas luchando contra el sistema.

Pero todo cambió con iOS 16. Apple introdujo NavigationStack, una API robusta, flexible y diseñada para el futuro.

En este tutorial masivo, vamos a desglosar todo lo que necesitas saber para crear flujos de navegación fluidos en Xcode. Olvídate de los tutoriales obsoletos; aquí aprenderás la “Nueva Forma” (The Modern Way), desde enlaces básicos hasta gestión de rutas complejas con datos dinámicos.


Parte 1: Los Fundamentos – Adiós NavigationView, Hola NavigationStack

Antes de escribir una sola línea de código, es vital entender el cambio de paradigma.

  • Antiguamente (NavigationView): Se comportaba de manera errática en iPads (pantalla dividida) y tenía problemas graves si querías ir a una pantalla específica desde código. (Actualmente está Deprecado).
  • Actualmente (NavigationStack): Es una pila literal de vistas. Pones una vista encima de otra. Es determinista, controlable y maneja los datos de forma excelente.

Tu Primera Navegación Básica

El componente más sencillo para moverse es el NavigationLink. Piensa en él como la etiqueta <a> de HTML o un botón (UIButton) que automáticamente realiza la acción de “Push” (deslizar la pantalla nueva desde la derecha).

import SwiftUI

struct HomeView: View {
    var body: some View {
        NavigationStack {
            VStack(spacing: 20) {
                Image(systemName: "map.fill")
                    .font(.system(size: 60))
                    .foregroundStyle(.blue)
                
                Text("Bienvenido al Explorador")
                    .font(.title)
                
                // El enlace básico
                NavigationLink("Ir al Detalle") {
                    DetalleView()
                }
                .buttonStyle(.borderedProminent)
            }
            .navigationTitle("Inicio")
        }
    }
}

struct DetalleView: View {
    var body: some View {
        Text("Estás en la vista de detalle")
            .font(.largeTitle)
            .navigationTitle("Detalle") // Título en la barra superior
            .navigationBarTitleDisplayMode(.inline) // Título pequeño
    }
}

Puntos Clave:

  1. El Contenedor: Todo debe estar envuelto en un NavigationStack. Si no lo pones, el enlace aparecerá deshabilitado (gris) y no funcionará.
  2. El Destino: En el ejemplo anterior, el destino (DetalleView) está “hardcodeado” (escrito directamente) dentro del enlace. Esto está bien para apps muy simples, pero es malo para la escalabilidad.

Parte 2: La Revolución – Navegación Basada en Valores (Data-Driven)

Aquí es donde SwiftUI brilla hoy en día. En lugar de decirle a la vista “Navega a la Vista X”, le dices “Aquí hay un dato (un valor), decide tú a dónde ir”.

Esto desacopla tu interfaz de tu lógica de navegación.

El modificador .navigationDestination

Imagina que tienes una lista de números. En lugar de crear un enlace que contenga la vista de destino para cada número (lo cual consume memoria), simplemente pasamos el número.

struct ListaNumerosView: View {
    let numeros = 1...20
    
    var body: some View {
        NavigationStack {
            List(numeros, id: \.self) { numero in
                // 1. El enlace solo lleva el DATOS (Int)
                NavigationLink("Ver número \(numero)", value: numero)
            }
            .navigationTitle("Contador")
            // 2. El destino se define UNA vez para todo el tipo de dato
            .navigationDestination(for: Int.self) { numeroSeleccionado in
                PantallaNumero(numero: numeroSeleccionado)
            }
        }
    }
}

struct PantallaNumero: View {
    let numero: Int
    var body: some View {
        Text("\(numero)")
            .font(.system(size: 100))
            .bold()
    }
}

¿Por qué es mejor esto?

  1. Rendimiento: SwiftUI no necesita instanciar las vistas de destino hasta que realmente haces clic.
  2. Organización: Tienes un único lugar donde gestionas a dónde va el usuario cuando selecciona un Int.

Manejando Múltiples Tipos de Datos

¿Qué pasa si en la misma lista quieres navegar a un “Perfil de Usuario” y a una “Configuración”? Fácil. Solo necesitas asegurarte de que tus datos sean Hashable.

// Definimos nuestros tipos de datos
struct Usuario: Hashable {
    let nombre: String
}

struct Ajuste: Hashable {
    let id: String
}

struct MultiNavegacionView: View {
    var body: some View {
        NavigationStack {
            VStack(spacing: 20) {
                NavigationLink("Ver Perfil de Ana", value: Usuario(nombre: "Ana"))
                NavigationLink("Ver Ajustes de Wifi", value: Ajuste(id: "Wifi"))
            }
            .navigationTitle("Panel de Control")
            // Destino para Usuarios
            .navigationDestination(for: Usuario.self) { usuario in
                Text("Perfil de \(usuario.nombre)")
                    .background(Color.yellow)
            }
            // Destino para Ajustes
            .navigationDestination(for: Ajuste.self) { ajuste in
                Text("Configurando: \(ajuste.id)")
                    .background(Color.gray)
            }
        }
    }
}

SwiftUI es lo suficientemente inteligente para detectar qué tipo de dato (value) estás enviando y elegir el .navigationDestination correcto.


Parte 3: Navegación Programática (El Santo Grial)

Esta es la pregunta más común en StackOverflow: “¿Cómo navego a la siguiente pantalla automáticamente después de que termine una descarga, sin que el usuario toque nada?”

Con NavigationStack, esto se maneja mediante el Path (Ruta).

Usando un NavigationPath

El NavigationStack acepta un parámetro llamado path. Este path es, esencialmente, un array (arreglo) de datos.

  • Si el array está vacío [], estás en la vista raíz.
  • Si añades un elemento al array, la app navega automáticamente a la vista correspondiente a ese elemento.
  • Si borras el último elemento, la app hace “Atrás” (Pop).
  • Si borras todo el array, la app vuelve al inicio (Pop to Root).
struct NavegacionProgramaticaView: View {
    // El estado que controla la navegación
    @State private var camino: [Int] = [] // Usamos un array simple de Enteros

    var body: some View {
        NavigationStack(path: $camino) {
            VStack {
                Button("Ir al nivel 1") {
                    camino.append(1) // ¡Magia! Navega automáticamente
                }
                
                Button("Ir directamente al nivel 3") {
                    camino.append(contentsOf: [1, 2, 3]) // Salta 3 pantallas de golpe
                }
            }
            .navigationTitle("Raíz")
            .navigationDestination(for: Int.self) { nivel in
                VistaNivel(nivel: nivel, camino: $camino)
            }
        }
    }
}

struct VistaNivel: View {
    let nivel: Int
    @Binding var camino: [Int]

    var body: some View {
        VStack(spacing: 20) {
            Text("Estás en el nivel \(nivel)")
                .font(.title)

            Button("Siguiente Nivel") {
                camino.append(nivel + 1)
            }
            
            Button("Volver al Inicio") {
                camino.removeAll() // Esto hace "Pop to Root"
            }
        }
    }
}

Este control total es lo que hace que NavigationStack sea superior a cualquier solución anterior. Puedes guardar este array en UserDefaults o en un archivo JSON y, al reiniciar la app, ¡el usuario aparecerá exactamente en la pantalla profunda donde se quedó! (State Restoration).


Parte 4: Navegación Modal (Sheets y FullScreenCover)

No toda navegación es “lateral” (Push). A veces necesitas presentar información temporal, formularios o configuraciones que aparecen desde abajo. Esto no se hace con NavigationStack directamente, sino con modificadores de estado.

.sheet (La hoja flotante)

Es la carta que aparece cubriendo casi toda la pantalla pero dejando ver un poco del fondo (en iOS). El usuario puede arrastrarla hacia abajo para cerrarla.

.fullScreenCover (Cobertura total)

Cubre toda la pantalla. El usuario NO puede cerrarla arrastrando; tú debes proveer un botón de “Cerrar”.

struct ModalExampleView: View {
    @State private var mostrarHoja = false
    @State private var mostrarPantallaCompleta = false

    var body: some View {
        VStack(spacing: 20) {
            Button("Abrir Hoja (Sheet)") {
                mostrarHoja = true
            }
            
            Button("Abrir Pantalla Completa") {
                mostrarPantallaCompleta = true
            }
        }
        // MODAL TIPO SHEET
        .sheet(isPresented: $mostrarHoja) {
            VStack {
                Text("Soy una hoja modal")
                Button("Cerrar") { mostrarHoja = false }
            }
            .presentationDetents([.medium, .large]) // iOS 16+: Controla la altura
        }
        // MODAL PANTALLA COMPLETA
        .fullScreenCover(isPresented: $mostrarPantallaCompleta) {
            // ¡Importante! Las modales no heredan el NavigationStack
            // Si quieres navegar DENTRO de la modal, necesitas uno nuevo.
            NavigationStack {
                VistaContenidoModal()
                    .toolbar {
                        ToolbarItem(placement: .cancellationAction) {
                            Button("Cerrar") { mostrarPantallaCompleta = false }
                        }
                    }
            }
        }
    }
}

Consejo Pro: Las vistas presentadas con .sheet o .fullScreenCover crean un contexto nuevo. Si quieres navegar dentro de ellas, debes envolver su contenido en su propio NavigationStack.


Parte 5: TabView – Navegación Jerárquica

La mayoría de las apps grandes (Instagram, Spotify, App Store) usan una barra de pestañas (TabBar) en la parte inferior.

La estructura correcta en SwiftUI es: TabView -> Tab -> NavigationStack -> Vistas

No pongas el TabView dentro de un NavigationStack. El TabView es el padre supremo.

struct AppPrincipal: View {
    var body: some View {
        TabView {
            // Pestaña 1
            NavigationStack {
                HomeView()
            }
            .tabItem {
                Label("Inicio", systemImage: "house")
            }
            
            // Pestaña 2
            NavigationStack {
                Text("Pantalla de búsqueda")
                    .navigationTitle("Buscar")
            }
            .tabItem {
                Label("Buscar", systemImage: "magnifyingglass")
            }
        }
    }
}

Esto permite que cada pestaña mantenga su propia historia de navegación. Si navegas profundamente en la pestaña “Inicio”, cambias a “Buscar” y vuelves a “Inicio”, seguirás en la pantalla profunda donde lo dejaste.


Parte 6: Personalización de la Barra de Navegación

Xcode nos da herramientas para tunear la barra superior (NavigationBar).

1. Título y Estilo

.navigationTitle("Mi Título")
.navigationBarTitleDisplayMode(.inline) // .large (por defecto) o .inline (pequeño)

2. Ocultar la barra (Modo Inmersivo)

Útil para pantallas de carga o reproductores de video.

.toolbar(.hidden, for: .navigationBar)

3. Botones en la barra (Toolbar)

Puedes añadir botones a la izquierda, derecha, o incluso en el teclado.

.toolbar {
    ToolbarItem(placement: .primaryAction) { // Derecha (o principal)
        Button(action: guardar) {
            Image(systemName: "square.and.arrow.down")
        }
    }
    
    ToolbarItem(placement: .cancellationAction) { // Izquierda (o cancelar)
        Button("Cancelar", action: cancelar)
    }
}

Parte 7: Errores Comunes y Mejores Prácticas

Para cerrar, evitemos los dolores de cabeza típicos.

El error del NavigationStack anidado

Nunca pongas un NavigationStack dentro de otro NavigationStack (a menos que sea dentro de un .sheet). Si lo haces, verás doble barra de navegación y comportamientos extraños. El Stack debe estar en la raíz de la jerarquía o raíz de la pestaña.

Pasar Datos vs. EnvironmentObject

  • Para pasar datos simples (un ID, un nombre, un struct pequeño), usa el value del NavigationLink.
  • Si necesitas que todas las pantallas accedan a un dato global (como el UsuarioLogueado), inyecta un @EnvironmentObjecto usa @Environment(MiData.self) (con la nueva macro @Observable de iOS 17).

¿Dónde va el modificador .navigationDestination?

Este es un error sutil. El modificador debe estar aplicado dentro del NavigationStack, pero generalmente se coloca al final de la vista raíz (como una Lista o un VStack principal), no en cada celda hija. Si lo pones dentro de un bucle ForEach, SwiftUI registrará el destino cientos de veces, ralentizando tu app.

Incorrecto:

List(items) { item in
   NavigationLink(value: item)
       .navigationDestination(...) // ¡MAL! Se repite por cada fila
}

Correcto:

List(items) { item in
   NavigationLink(value: item)
}
.navigationDestination(...) // ¡BIEN! Se define una sola vez

Conclusión

La navegación en SwiftUI ha madurado. Ya no es necesario pelear con booleanos ocultos (isActive) o trucos sucios para hacer que la app funcione. Con NavigationStacknavigationDestination y el manejo de rutas mediante arrays, tienes el poder de crear flujos de usuario complejos, robustos y, sobre todo, mantenibles.

Tu siguiente paso es abrir Xcode, crear un proyecto nuevo y tratar de replicar un flujo de “Login -> Home -> Detalle -> Carrito”. Una vez domines el flujo de datos, dominarás SwiftUI.

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

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

Mejores temas para Xcode

Next Article

Mejores extensiones para Xcode

Related Posts