La navegación es la columna vertebral de cualquier app. Hasta iOS 15, SwiftUI ofrecía NavigationView, que tenía limitaciones importantes: no permitía navegar de forma programática a destinos arbitrarios, no soportaba deep linking de forma limpia y su comportamiento en iPad era impredecible. iOS 16 introdujo NavigationStack y NavigationPath, una reescritura completa que convierte la navegación en un estado de primer orden, como cualquier otra parte del modelo de datos de la app.
NavigationStack: el stack de navegación como estado
La diferencia fundamental con NavigationView es que el historial de navegación ahora es un array que puedes leer y modificar directamente:
struct AppRaiz: View {
@State private var ruta: [DestinoApp] = []
var body: some View {
NavigationStack(path: $ruta) {
ListaCategorias()
.navigationDestination(for: Categoria.self) { categoria in
ListaProductos(categoria: categoria)
}
.navigationDestination(for: Producto.self) { producto in
DetalleProducto(producto: producto)
}
}
}
}
Cuando el usuario toca un NavigationLink, el destino se añade a ruta. Cuando pulsa atrás, se elimina. Y puedes manipular ese array desde cualquier parte de tu código.
navigationDestination: registro de destinos
navigationDestination(for:destination:) es el mecanismo que conecta los tipos de datos con las vistas que los representan. Puedes registrar múltiples destinos en el stack:
NavigationStack(path: $ruta) {
VistaInicial()
.navigationDestination(for: Categoria.self) { categoria in
ListaProductos(categoria: categoria)
}
.navigationDestination(for: Producto.self) { producto in
DetalleProducto(producto: producto)
}
.navigationDestination(for: Pedido.self) { pedido in
DetallePedido(pedido: pedido)
}
}
Los NavigationLink dentro del stack usan el tipo del valor para seleccionar automáticamente el destino correcto:
// En ListaProductos:
ForEach(productos) { producto in
NavigationLink(value: producto) { // valor tipo Producto
ProductoRow(producto: producto)
}
}
NavigationPath: stack heterogéneo con type erasure
Cuando el historial de navegación puede contener tipos distintos de forma dinámica, [DestinoApp] (un enum) funciona bien. Pero si necesitas un stack truly heterogéneo donde cualquier tipo Hashable pueda ser un destino, usa NavigationPath:
@State private var ruta = NavigationPath()
// Navegar a distintos tipos
ruta.append(Categoria(id: 1))
ruta.append(Producto(id: 42))
ruta.append(Pedido(id: 99))
// Volver N pantallas atrás
ruta.removeLast(2)
// Ir a la raíz
ruta.removeLast(ruta.count)
NavigationPath borra el tipo internamente (type erasure) para poder almacenar valores de tipos distintos en el mismo array.
Navegación programática
El poder real de este sistema llega cuando navegas desde el código sin que el usuario toque ningún enlace:
@Observable
class NavegacionViewModel {
var ruta: [DestinoApp] = []
func irAlProducto(_ producto: Producto) {
ruta = [.categoria(producto.categoria), .producto(producto)]
}
func volverAlInicio() {
ruta.removeAll()
}
func irACheckout(pedido: Pedido) {
ruta.append(.pedido(pedido))
}
}
struct AppRaiz: View {
@State private var navModel = NavegacionViewModel()
var body: some View {
NavigationStack(path: $navModel.ruta) {
VistaInicial()
.navigationDestination(for: DestinoApp.self) { destino in
destino.vistaAsociada()
}
}
.environment(navModel)
}
}
Deep linking con URLs externas
Una de las ventajas más importantes es el deep linking limpio. Cuando la app recibe una URL externa, puedes traducirla a un estado de navegación y aplicarlo directamente:
struct AppRaiz: View {
@State private var ruta: [DestinoApp] = []
var body: some View {
NavigationStack(path: $ruta) {
VistaInicial()
.navigationDestination(for: DestinoApp.self) { $0.vista() }
}
.onOpenURL { url in
if let destinos = RouterApp.parsear(url) {
ruta = destinos
}
}
}
}
struct RouterApp {
static func parsear(_ url: URL) -> [DestinoApp]? {
// URL: miapp://producto/42
guard url.scheme == "miapp" else { return nil }
let partes = url.pathComponents.dropFirst()
switch url.host {
case "producto":
if let idStr = partes.first, let id = Int(idStr),
let producto = BDLocal.shared.producto(id: id) {
return [.producto(producto)]
}
case "categoria":
if let nombre = partes.first,
let cat = BDLocal.shared.categoria(nombre: nombre) {
return [.categoria(cat)]
}
default: break
}
return nil
}
}
NavigationSplitView: iPad y macOS
Para pantallas grandes donde tiene sentido una columna lateral, NavigationSplitView es la pieza complementaria:
struct AppPrincipal: View {
@State private var categoriaSeleccionada: Categoria?
@State private var productoSeleccionado: Producto?
var body: some View {
NavigationSplitView {
// Sidebar: lista de categorías
ListaCategorias(seleccion: $categoriaSeleccionada)
} content: {
// Columna central: productos de la categoría
if let cat = categoriaSeleccionada {
ListaProductos(categoria: cat, seleccion: $productoSeleccionado)
} else {
Text("Selecciona una categoría").foregroundStyle(.secondary)
}
} detail: {
// Detalle: producto seleccionado
if let producto = productoSeleccionado {
DetalleProducto(producto: producto)
} else {
Text("Selecciona un producto").foregroundStyle(.secondary)
}
}
}
}
En iPhone, NavigationSplitView se comporta como un NavigationStack automáticamente. En iPad y Mac muestra las columnas en pantalla.
Persistir el estado de navegación
NavigationPath admite codificación si todos sus tipos conforman Codable. Esto permite persistir y restaurar el stack de navegación entre sesiones:
extension NavigationPath {
func guardar() -> Data? {
try? codable.flatMap { try JSONEncoder().encode($0) }
}
static func cargar(datos: Data) -> NavigationPath {
guard let representacion = try? JSONDecoder().decode(
NavigationPath.CodableRepresentation.self, from: datos
) else { return NavigationPath() }
return NavigationPath(representacion)
}
}
Resumen
NavigationStack convierte la navegación en estado explícito y manipulable. La combinación de navigationDestination para registrar destinos, NavigationPath para stacks heterogéneos y NavigationSplitView para pantallas grandes cubre todos los escenarios de navegación de una app real. El deep linking y la navegación programática, que antes requerían hacks, se vuelven derivaciones naturales del mismo modelo.
