SwiftUI: componentes NavigationView y NavigationLink
Qué son NavigationView y NavigationLink
NavigationView
El componente NavigationView
es un contenedor de vistas que permite crear una pila de navegación de pantallas que permite navegar entre ellas. Es un componente equivalente a UINavigationController
de UIKit
.
Aquí podéis consultar la documentación oficial
Es un componente nativo del sistema muy importante que normalmente estará presente en el 100% de los proyectos. Visualmente el componente tiene 3 partes:
NavigationBar
. Barra superior que a su vez se compone de tres partes:Leading Buttons.
Botones para realizar acciones sobre la pantalla. Por lo general, aquí encontraremos el botónback
que proporciona el propio sistema cuando estamos en un nivel de profundidad de la pila de pantallas superior a la primera.Title
. Título de la pantalla. Puede componerse de dos textos en el caso de losNavigationBar
de tipolarge
Trailing Buttons
: Botones para realizar acciones sobre la pantalla.
Container
. Vista a mostrar es la pantalla que debemos cargar con la información que queramos.Toolbar
. Barra inferior que permite añadir más botones para realizar acciones sobre la pantalla.
Estas propiedades se pueden personalizar a través de unos modificadores, que veremos más adelante.
Para usar el componente lo haremos de la siguiente forma:
NavigationView { ContentView() //This is your View to present }
El componente solo tiene un closure de tipo ViewBuilder
en el que se debe incluir la vista que queremos mostrar. Encapsulando una vista dentro del NavigationView
tendremos la capacidad de mostrar u ocultar el NavigationBar
y el Toolbar
a través de modificadores y realizar la navegación entre pantallas.
struct ContentView: View { var body: some View { NavigationView { Group { Text("Hello, World!") } .navigationBarTitle("Simple", displayMode: .inline) .navigationBarItems(trailing: HStack { Button("Button 1") { //Do something } Button("Button 2") { //Do something } }) .toolbar(items: { ToolbarItem(placement: .bottomBar) { HStack { Button("First") { //Do something } Button("Second") { //Do something } } } }) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .background(Color.yellow.opacity(0.3)) } } }
El NavigatinoView
solo se incluye en la pantalla que inicia la navegación y no es necesario que las vistas a las que se navega incluyan otro NavigationView
, ya que iniciaría una pila de navegación nueva.
struct ContentView: View { var body: some View { NavigationView { VStack(spacing: 20) { Text("First View") NavigationLink("Go to second view", destination: ContentSecondView()) } .navigationBarTitle("First", displayMode: .inline) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .background(Color.yellow.opacity(0.3)) } } } //No contains NavigationView struct ContentSecondView: View { var body: some View { Group { Text("Second View") } .navigationBarTitle("Second", displayMode: .inline) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .background(Color.green.opacity(0.3)) } }
NavigationLink
Para realizar la navegación entre pantallas tendremos que usar el componente NavigationLink
. Este componente es equivalente a usar los métodos pushViewController
o popViewController
de UINavigationController
de UIKit
.
Aquí podéis consultar la documentación oficial
Tiene el aspecto visual de un botón, pero su acción esta prefijada para que realice una navegación a la vista indicada.
struct ContentView: View { var body: some View { NavigationView { VStack(spacing: 20) { Text("First View") NavigationLink("Go to second view", destination: ContentSecondView()) } .navigationBarTitle("First", displayMode: .inline) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .background(Color.yellow.opacity(0.3)) } } } struct ContentSecondView: View { var body: some View { Group { Text("Second View") } .navigationBarTitle("Second", displayMode: .inline) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .background(Color.green.opacity(0.3)) } }
En este ejemplo el punto de entrada de la aplicación es la vista ContentView
. Esta vista tiene un NavigationLink
que navegará a la pantalla ContentSecondView
y desde esta se podrá volver a la pantalla anterior pulsando sobre el botón < First
, que aparecerá en el NavigationBar
a la izquierda. Este botón lo proporciona el propio NavigationView
al realizar la navegación.
Tal y como se ve, el funcionamiento de NavigationLink
es muy sencillo en su forma básica, pero hay ocasiones que necesitaremos poder realizar una navegación o volver atrás sin tener que pulsar los botones que nos proporciona la propia SDK.
Cómo navegar sin pulsar un NavigationLink
Hay ocasiones que el desencadenante de la navegación puede ser algo ajeno a la pulsación del NavigationLink
o al botón back
del NavigationBar
para volver atrás. Para estos casos tendremos que seguir usando el componente NavigationLink
, pero con una serie de modificadores que nos permitirán activar o desactivar la navegación que ellos implementan.
struct ContentView: View { @State var navigateToSecond = false var body: some View { NavigationView { Group { VStack(spacing: 20) { Text("First View") Button("Go to second view") { navigateToSecond = true } } .background( NavigationLink( destination: Text("Second View"), isActive: $navigateToSecond, label: { EmptyView() }) .hidden() ) .navigationBarTitle("First", displayMode: .inline) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .background(Color.yellow.opacity(0.3)) } } } }
Como vemos, en esta ocasión el NavigationLink
no se muestra porque devuelve una EmptyView
. De esta forma es imposible pulsarlo.
También vemos que el parámetro isActive
tiene una variable de estado de tipo Bool
llamada navigateToSecond
. Esta variable isActive
es la encargada de indicar si se debe realizar la navegación (true
) o no (false
). Por lo tanto, lo único que necesitamos es un botón que modifique el estado de navigateToSecond
a true
para iniciar la navegación.
De la misma forma, si la pantalla destino (u otras partes del código) fuera capaz de modificar el valor de esta variable navigateToSecond
a false
, la navegación volvería atrás. Este caso lo vamos a ver a continuación.
Más ejemplos
Vamos a crear un ejemplo con tres pantallas donde no vamos a navegar con el NavigationLink
directamente, ni con los botones back
que crea el NavigationView
.
ContentView
: podrá navegar aContentSecondView
y aContentThirdView
.ContentSecondView
: podrá volver a atrás y navegar aContentThirdView
.ContentThirdView
: navegará atrás automáticamente pasados 5 segundos y podrá volver a la primera pantalla directamente cuando viene desdeContentSecondView
.
import SwiftUI import Combine struct ContentView: View { @State var navigateToSecond = false @State var navigateToThird = false var body: some View { NavigationView { Group { VStack(spacing: 20) { Text("First View") Button("Go to second view") { navigateToSecond = true } Button("Go to third view") { navigateToThird = true } } .background( Group { NavigationLink( destination: ContentSecondView(navigateToSecond: $navigateToSecond), isActive: $navigateToSecond, label: { EmptyView() }) NavigationLink( destination: ContentThirdView(navigateToThird: $navigateToThird), isActive: $navigateToThird, label: { EmptyView() }) } .hidden() ) .navigationBarTitle("First", displayMode: .inline) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .background(Color.yellow.opacity(0.3)) } } } } struct ContentSecondView: View { var navigateToSecond: Binding@State var navigateToThird = false var body: some View { VStack(spacing: 20) { Text("Second View") Button("Go to third view") { navigateToThird = true } Button("Back") { navigateToSecond.wrappedValue = false } } .background( NavigationLink( destination: ContentThirdView(navigateToSecond: navigateToSecond, navigateToThird: $navigateToThird), isActive: $navigateToThird, label: { EmptyView() }) ) .navigationBarTitle("Second", displayMode: .inline) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .background(Color.green.opacity(0.3)) } } struct ContentThirdView: View { var navigateToSecond: Binding ? @Binding var navigateToThird: Bool @ObservedObject private var vmTimer = TimerViewModel() var body: some View { VStack(spacing: 20) { Text("Third View") Text("Back automatically in \(5 - vmTimer.seconds) seconds") .onChange(of: vmTimer.seconds) { seconds in if seconds == 5 { navigateToThird = false } } if let navigateToSecond = navigateToSecond { Button("Back to root") { navigateToSecond.wrappedValue = false } } } .navigationBarTitle("Third", displayMode: .inline) .navigationBarBackButtonHidden(true) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .background(Color.red.opacity(0.3)) .onAppear { self.vmTimer.setup() } .onDisappear { self.vmTimer.cleanup() } } } class TimerViewModel: ObservableObject { @Published var seconds = 0 var subscriber: AnyCancellable? func setup() { self.seconds = 0 self.subscriber = Timer .publish(every: 1, on: .main, in: .common) .autoconnect() .sink(receiveValue: { _ in self.seconds += 1 }) } func cleanup() { self.subscriber = nil } }
Para realizar la navegación tendremos que crear los NavigationLink
que necesitemos, pero en este caso los crearemos ocultos (devolviendo un EmptyView
). Estos NavigationLink
tienen un parámetro de tipo Bool
llamado isActive
que indica si se ha realizado la navegación o no. Este valor es clave para realizar la navegación:
- Cuando el valor es
true
se realiza la navegación a la vista indicada en elNavigationLink
. - Cuando el valor es
false
se regresa a la pantalla que contiene elNavigationLink
.
Por lo tanto, este valor es una variable de estado que tendrá que pasarse a la pantalla de destino para que desde ésta podamos volver atrás 'programáticamente' 'seteando' su valor a false.
Esta variable de estado la podemos traspasar a la vista que deseemos (a través de varias vistas si queremos), lo que provocará que cuando pongamos su valor a false
volvamos a la vista que contenga el NavigationLink
, como en el ejemplo de la variable navigateToSecond
de ContentThirdView
.
Modificadores comunes para NavigationView
A parte de los modificadores que se explicarán a continuación, el componente NavigationView
comparte los mismos métodos de personalización que el componente View
y pueden ser consultados en el siguiente enlace.
navigationBarTitle
Permite indicar el título y el tipo de presentación de la barra de navegación superior.
NavigationView { VStack(spacing: 15) { Text("Hello, World!") Text("navigationBarTitle") } .navigationBarTitle("Custom title and displayMode", displayMode: .large) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .background(Color.yellow.opacity(0.3)) }
El parámetro displayMode
permite que la barra de navegación se muestre extendida (normalmente se usa para pantallas de primer nivel) o compacta.
Para que este modificador tenga efecto sobre el NavigationView se debe aplicar sobre una vista que esté dentro de él y no sobre el propio componente.
navigationBarTitleDisplayMode
Permite indicar el tipo de presentación de la barra de navegación superior.
NavigationView { VStack(spacing: 15) { Text("Hello, World!") Text("navigationBarTitleDisplayMode") } .navigationBarTitle("Custom displayMode") .navigationBarTitleDisplayMode(.inline) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .background(Color.yellow.opacity(0.3)) }
El parámetro displayMode
permite que la barra de navegación se muestre extendida (normalmente se usa para pantallas de primer nivel) o compacta.
Para que este modificador tenga efecto sobre el NavigationView se debe aplicar sobre una vista que esté dentro de él y no sobre el propio componente.
navigationViewStyle
Permite seleccionar el modo de presentación de la pila de navegación. Con este modificador podemos conseguir una presentación clásica como UINavigationController
, donde solo se mostrará la vista que esté más arriba de la pila de navegación, o como un UISplitViewController
, donde se mostrarán las vistas a doble columna cuando el contexto lo permita (cómo en iPad).
NavigationView { VStack(spacing: 15) { Text("Hello, World!") Text("navigationViewStyle") } .navigationViewStyle(DoubleColumnNavigationViewStyle()) .navigationBarTitle("Custom navigationViewStyle") .navigationBarTitleDisplayMode(.inline) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .background(Color.yellow.opacity(0.3)) }
Los valores de navigationViewStyle
tienen que implementar el protocolo NavigationViewStyle.
Podéis encontrar su documentación aquí.
Por defecto podemos encontrar las siguientes implementaciones:
StackNavigationViewStyle
. Estilo que solo muestra la última vista del stack de navegación.DoubleColumnNavigationViewStyle
. Estilo que muestra una doble columna con un stack que navega a una vista de detalle.DefaultNavigationViewStyle
. Este estilo alterna entre los dos anteriores, dependiendo del dispositivo donde se ejecute. En iPhone usaráStackNavigationViewStyle
, mientras que en iPad usaráDoubleColumnNavigationViewStyle
.
Importante: actualmente el estilo DoubleColumnNavigationViewStyle
tiene algunas limitaciones de uso que no lo equiparan completamente a un UISplitViewController
. Podéis ver más información en este hilo.
navigationBarBackButtonHidden
Permite ocultar el botón de navegación que proporciona el componente NavigationView
para volver a la pantalla anterior. Este botón nunca aparece cuando es la primera pantalla del stack.
struct ContentView: View { @State var showDetail: Bool = false var body: some View { VStack { NavigationView { VStack(spacing: 15) { Text("Hello, World!") Text("navigationBarBackButtonHidden") NavigationLink("Show Detail", destination: ContentSecondView(showDetail: $showDetail), isActive: $showDetail) } .navigationViewStyle(StackNavigationViewStyle()) .navigationBarTitle("First", displayMode: .inline) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .background(Color.yellow.opacity(0.3)) } .border(Color.black, width: 1) } .navigationBarTitle("Style 3", displayMode: .inline) .navigationColor(background: UIColor(red: 31/255, green: 155/255, blue: 222/255, alpha: 1), title: .white) .padding() } } struct ContentSecondView: View { @Binding var showDetail: Bool var body: some View { VStack { VStack(spacing: 15) { Text("Hello, World!") Text("navigationBarBackButtonHidden") Button("Back") { showDetail.toggle() } } .navigationBarTitle("Second", displayMode: .inline) .navigationBarBackButtonHidden(true) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .background(Color.yellow.opacity(0.3)) } } }
Para que este modificador tenga efecto sobre el NavigationView se debe aplicar sobre una vista que esté dentro de él, y no sobre el propio componente.
navigationBarItems
Permite personalizar los botones de la barra de navegación superior del NavigationView
. Podemos personalizar los botones que aparecerán a la derecha o a la izquierda de la barra de navegación con vistas personalizadas.
NavigationView { VStack(spacing: 15) { Text("Hello, World!") Text("navigationBarItems") } .navigationViewStyle(StackNavigationViewStyle()) .navigationBarTitle("First", displayMode: .inline) .navigationBarItems(leading: HStack { Button("Left 1") { //Do Something } .buttonStyle(PlainButtonStyle()) Button("Left 2") { //Do Something } }, trailing: HStack { Button("Right 1") { //Do Something } Button("Right 2") { //Do Something } }) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .background(Color.yellow.opacity(0.3)) }
Para que este modificador tenga efecto sobre el NavigationView se debe aplicar sobre una vista que esté dentro de él, y no sobre el propio componente.
toolbar
(iOS 14)
Este nuevo modificador introducido en iOS 14 nos permite mayor personalización sobre la barra de navegación del NavigationView
. Actúa como un sustituto del modificador navigationBarItems
y, además, permite un mayor nivel de personalización como, por ejemplo, añadir una toolbar inferior en la vista.
NavigationView { VStack(spacing: 15) { Text("Hello, World!") Text("navigationBarItems") } .navigationViewStyle(StackNavigationViewStyle()) .navigationBarTitle("First", displayMode: .inline) .toolbar(content: { ToolbarItem(placement: .bottomBar) { HStack { Button("First") { //Do something } Button("Second") { //Do something } } } ToolbarItem(placement: .navigationBarTrailing) { HStack { Button("Right 1") { //Do something } Button("Right 2") { //Do something } } } }) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .background(Color.yellow.opacity(0.3)) }
Para que este modificador tenga efecto sobre el NavigationView se debe aplicar sobre una vista que esté dentro de él, y no sobre el propio componente.
Modificadores comunes para NavigationLink
A efectos visuales el componente NavigationLink
se comporta como un Button
, por lo que comparte los mismos modificadores que este componente. Puedes consultar su documentación aquí.
Cómo modificar el color del NavigationBar de NavigationView
Actualmente no hay una API para la personalización del NavigationBar
de un NavigationView
, por lo que para conseguir personalizar su aspecto visual tenemos que recurrir al protocolo UIViewControllerRepresentable
, y así conseguir aplicar la personalización.
Nuestro objetivo será extraer el componente UINavigationBar
que se crea cuando usamos un NavigationView
, y de esta forma poder personalizarlo como si se tratase de un componente de UIKit
.
import SwiftUI struct NavigationConfigurator: UIViewControllerRepresentable { let backgroundColor: UIColor let titleColor: UIColor func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIViewController { context.coordinator } func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext ) { } func makeCoordinator() -> Coordinator { Coordinator(backgroundColor: backgroundColor, titleColor: titleColor) } class Coordinator: UIViewController { let backgroundColor: UIColor let titleColor: UIColor init(backgroundColor: UIColor, titleColor: UIColor) { self.backgroundColor = backgroundColor self.titleColor = titleColor super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) var items = [UINavigationController]() if let sv = self.splitViewController { if let nc = sv.viewControllers.last as? UINavigationController, !items.contains(nc), sv.viewControllers.first != self.navigationController { items.append(nc) } } if let nc = self.navigationController, !items.contains(nc) { items.append(nc) } applyStyle(items) } private func applyStyle(_ items: [UINavigationController]) { items.forEach{ nc in //Without this, appearance not applied let last = nc.navigationBar.barTintColor nc.navigationBar.barTintColor = .red nc.navigationBar.barTintColor = .white nc.navigationBar.barTintColor = last //---------- let navBarAppearance = UINavigationBarAppearance() navBarAppearance.configureWithOpaqkground() navBarAppearance.titleTextAttributes = [.foregroundColor: self.titleColor] navBarAppearance.largeTitleTextAttributes = [.foregroundColor: self.titleColor] navBarAppearance.backgroundColor = self.backgroundColor nc.navigationBar.standardAppearance = navBarAppearance nc.navigationBar.scrollEdgeAppearance = navBarAppearance nc.navigationBar.compactAppearance = navBarAppearance } DispatchQueue.main.async { items.forEach{ nc in nc.navigationBar.tintColor = self.titleColor } } view.setNeedsLayout() view.layoutIfNeeded() } } }
En nuestro ejemplo el componente NavigationConfigurator
permitirá personalizar el color de fondo y del texto. Para ello, en el método viewWillAppear
extraemos el UINavigationController
que tiene la vista para poder aplicarle los estilos en el método applyStyle
.
Para usarlo vamos a crear una extensión de View
para que sea más fácil.
import SwiftUI extension View { func navigationColor(background: UIColor, title: UIColor) -> some View { return self .background(NavigationConfigurator(backgroundColor: background, titleColor: title)) } }
De esta forma sólo tendríamos que invocar al modificador navigationColor
sobre una vista que contenga el NavigationView
.
struct ContentFirstView: View { var body: some View { NavigationView { VStack(spacing: 20) { Text("Hello, World!") NavigationLink("Go to Second", destination: ContentSecondView()) } .navigationBarTitle("First", displayMode: .large) .navigationColor(background: UIColor.green.withAlphaComponent(0.5), title: .white) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .background(Color.yellow.opacity(0.3)) } } } struct ContentSecondView: View { var body: some View { Group { Text("Hello, World!") } .navigationBarTitle("Second", displayMode: .inline) .navigationColor(background: UIColor.blue.withAlphaComponent(0.5), title: .white) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .background(Color.red.opacity(0.3)) } }
Ejemplo
Puedes encontrar este ejemplo en github.com bajo el apartado NavigationView
Rafael Fernández,
iOS Tech Lider