NavigationStack en SwiftUI 4, nueva navegación de apps en SwiftUI
NavigationStack en SwiftUI 4, nueva navegación de apps en SwiftUI

NavigationStack en SwiftUI #WWDC22

NavigationStack es una nueva vista añadida en SwiftUI 4 para mejorar la navegación de nuestras apps. Han introducido varias mejoras comparándolo con su deprecada versión, que era el NavigationView. En el post de hoy exploramos esta nueva API de NavigationStack junto con sus modificadores.

SwiftBeta

Tabla de contenido

Navega a distinas pantalla de tu app con NavigationStack y SwiftUI 4

Hoy en SwiftBeta vamos a ver la nueva API de navegación para apps con SwiftUI. A partir de iOS 16 podremos acceder a esta nueva vista llamada NavigationStack. Vamos a ver los cambios que han introducido respecto a NavigationView.

Si quieres apoyar al canal, puedes suscribirte, de esta manera seguiré subiendo contenido semanal sobre Swift, SwiftUI, Xcode y mucho más.

Aquí podrás encontrar todos los cambios introducidos en SwiftUI

Apple Developer Documentation

Creamos proyecto en Xcode 14

Lo primero de todo que vamos hacer es crear un proyecto de 0 con Xcode 14. Acuerdate de seleccionar SwiftUI.

Nos vamos a nuestro ContentView y lo que vamos hacer es añadir el NavigationStack, y dentro del NavigationStack vamos a añadir la vista List. Pero que pasa? necesitamos crear una colección de datos para poder mostrar en nuestro List:

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List()
            }
        }
    }
}

Así que vamos a crear nuestra colección, vamos a crear una Struct llamada Fruit con dos propiedades, una será el id (ya que debemos conformar el protocolo Hashable) y la otra será un nombre:

struct Fruit: Hashable, Identifiable {
    var id = UUID()
    var name: String
}

A partir de aquí generamos un array, una colección de frutas:

var fruits: [Fruit] = [
    .init(name:"🍊 Orange"),
    .init(name:"🍏 Apple"),
    .init(name:"🍒 Cherries"),
    .init(name:"🍌 Banana"),
    .init(name:"🍓 Strawberry"),
    .init(name:"🍉 Watermelon"),
    .init(name:"🍋 Lemon"),
    .init(name:"🫐 Blueberries")
]

Ahora ya podemos añadir este array fruits a nuestro List, aprovechamos y añadimos un NavigationLink:

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List(fruits) { fruit in
                NavigationLink(fruit.name) {
                    Text(fruit.name)
                }
            }
            .navigationTitle("Fruits")
        }
    }
}

Si compilamos, podemos ver que el array de frutas aparece en nuestro List, y que si pulsamos una fruta, tenemos una navegación push a una vista que solo contiene un Text (con el nombre de la fruta).

Pues bien, en iOS 16 Apple ha cambiado esta API, si te fijas lo único que hemos hecho ha sido sustituir nuestro NavigationView por el NavigationStack. Pero ahora vamos a utilizar las mejoras que ha introducido Apple en iOS 16.

Ahora en NavigationLink tenemos otro parámetro, en él le pasamos el valor, en nuestro caso es fruit:

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List(fruits) { fruit in
                NavigationLink(fruit.name, value: fruit)
            }
            .navigationTitle("Fruits")
        }
    }
}

Si ahora compilamos nuestra app no va a funcionar, ¿por qué? por que no le hemos especificado aún el destino. Es decir, al pulsar una celda de nuestro List, no sabe a dónde navegar. Y lo vamos a corregir con el siguiente modificador .navigationDestination(for:)

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List(fruits) { fruit in
                NavigationLink(fruit.name, value: fruit)
            }
            .navigationTitle("Fruits")
            .navigationDestination(for: Fruit.self) { fruit in
                Text(fruit.name)
            }
        }
    }
}

Si ahora compilamos podemos navegar al pulsar en una celda de List. Hasta aquí todo muy simple, el modificador navigationDestination espera un tipo, y según el tipo se navega a una vista. Vamos a añdir más tipos de nuestro List para ver que sigue otra ruta al detectar que no es del tipo Fruit. Para hacerlo nos nos vamos a complicar y vamos a añadir un pequeño banner, una vista, justo encima del List.

Vamos a crear un nuevo tipo llamado Developer, y va a tener una sola propiedad:

struct Developer: Hashable {
    let name: String
}

una vez creado, vamos a crear una instancia y vamos a crear un Form para que nuestra vista quede más elegante. Allí dentro vamos a añadir un nuevo NavigationLink

struct ContentView: View {
    let developer: Developer = .init(name: "SwiftBeta")
    
    var body: some View {
        NavigationStack {
            Form {
                Section {
                    NavigationLink(developer.name, value: developer)
                }
                Section {
                    List(fruits) { fruit in
                        NavigationLink(fruit.name, value: fruit)
                    }
                    .navigationTitle("Fruits")
                    .navigationDestination(for: Fruit.self) { fruit in
                        Text(fruit.name)
                    }
                }
            }    
        }
    }
}

Ahora solo faltaría añadir el modificador navigationDestination con el nuevo tipo Developer que acabamos de crear. Es decir, quedaría de la siguiente manera nuestro código:

struct ContentView: View {
    let developer: Developer = .init(name: "SwiftBeta")
    
    var body: some View {
        NavigationStack {
            Form {
                Section {
                    NavigationLink(developer.name, value: developer)
                }
                Section {
                    List(fruits) { fruit in
                        NavigationLink(fruit.name, value: fruit)
                    }
                    .navigationTitle("Fruits")
                    .navigationDestination(for: Fruit.self) { fruit in
                        Text(fruit.name)
                    }
                }
            }
            .navigationDestination(for: Developer.self) { developer in
                VStack {
                    Image(systemName: "laptopcomputer")
                    Text(developer.name)
                }
                .font(.largeTitle)
            }
                
        }
    }
}

Si compilamos, podemos pulsar en la primera sección (en la que solo tenemo un valor). Y si pulsamos vemos que navegamos a una vista con un VStack, Image y Text. Y si pulsamos en cualquier otra celda navegamos a una vista que solo tiene un Text. Esta mejora es muy potente, pero no es la única.

La vista NavigationStack tiene un parámetro que se lo podemos asignar, se llama path. Dentro de este parámetro le podemos pasar una serie de vistas que ya se han añadido al stack, por lo tanto cuando presentemos esa vista con un path, por cada elemento del path será una vista más. Vamos a verlo con un ejemplo, imagina que al cargar tu vista ContentView, quieres que por defecto se navegue directamente a varias vistas:

struct ContentView: View {
    @State private var path = [fruits[7], fruits[0]]
    let developer: Developer = .init(name: "SwiftBeta")
    
    var body: some View {
        NavigationStack(path: $path) {
            Form {
                Section {
                    List(fruits) { fruit in
                        NavigationLink(fruit.name, value: fruit)
                    }
   
                }
            }
            .navigationTitle("Fruits")
            .navigationDestination(for: Fruit.self) { fruit in
                Text(fruit.name)
            }
        }
    }
}

Si nosotros compilamos ahora mismo, vemos que al inicializar la app aparece una vista que muestra Orange y si le damos a back muestra Blueberries. Y al darle a back en Blueberries volvemos a la root view, la que sería la vista inicial de nuestra app. Pero imagina que dentro del array path, de la ruta que quieres crear de jerarquía de vistas, quieres meter otro tipo que noes Fruit, para ello necesitas crear un NavigationPath, este tipo, digamos que no tiene en cuenta los tipos y permite añadir diferentes instancias.

El código quedaría de la siguiente manera:

struct ContentView: View {
    @State private var path = NavigationPath()
    let developer: Developer = .init(name: "SwiftBeta")
    
    var body: some View {
        NavigationStack(path: $path) {
            Form {
                Section {
                    NavigationLink(developer.name, value: developer)
                }
                Section {
                    List(fruits) { fruit in
                        NavigationLink(fruit.name, value: fruit)
                    }
   
                }
            }
            .navigationTitle("Fruits")
            .navigationDestination(for: Fruit.self) { fruit in
                Text(fruit.name)
            }
            .navigationDestination(for: Developer.self) { developer in
                VStack {
                    Image(systemName: "laptopcomputer")
                    Text(developer.name)
                }
                .font(.largeTitle)
            }
        }
        .onAppear {
            path = NavigationPath([fruits[7], fruits[0]])
            path.append(developer)
        }
    }
}

Si compilamos vemos que aparece directamente la vista asignada a los tipo Developer, y luego aparecen las dos frutas, Blueberries y Orange.

Os quería comentar una última parte, en la documentación de Apple aparece que se puede modificar el path. Por ejemplo si estamos en una vista de la jerarquía de nuestro NavigationStack, sería posible borrar todos los elementos del path para volver a la vista root (la vista inicial). Pero probandolo y debuggando no he comprobarlo en el simulador PERO sí en el canvas, aquí tenéis el código donde añado un Button para borrar todos los elementos del path:

struct ContentView: View {
    @State private var path = [fruits[7], fruits[0]]
    let developer: Developer = .init(name: "SwiftBeta")
    
    var body: some View {
        NavigationStack(path: $path) {
            Form {
                Section {
                    NavigationLink(developer.name, value: developer)
                }
                Section {
                    List(fruits) { fruit in
                        NavigationLink(fruit.name, value: fruit)
                    }
   
                }
            }
            .navigationTitle("Fruits")
            .navigationDestination(for: Fruit.self) { fruit in
                VStack {
                    Text(fruit.name)
                    Button("Back to Root View") {
                        path.removeAll()
                    }
                    .padding(.top, 12)
                    .buttonStyle(.borderedProminent)
                    .font(.body)
                }
            }
            .navigationDestination(for: Developer.self) { developer in
                VStack {
                    Image(systemName: "laptopcomputer")
                    Text(developer.name)
                }
                .font(.largeTitle)
            }
        }
    }
}

Conclusión

Hoy hemos explorado una de las nuevas APIs que nos ofrece Apple para la navegación. Habrá que ver como encaja este sistema en aplicaciones grandes, con muchas transiciones y vistas.

Si quieres seguir aprendiendo sobre SwiftUI, Swift, Xcode, o cualquier tema relacionado con el ecosistema Apple