DEV Community

GoyesDev
GoyesDev

Posted on

[SUI] NavigationLink

NavigationStack maneja una pila de vistas. NavigationLink es un componente que crea un botón con cierta etiqueta, que el usuario puede presionar para agregar una vista en la pila.

  • init(_:destination:). titleKey es el texto de la etiqueta. destination es la vista a presentar.
  • init(_:value:). titleKey es el texto de la etiqueta. value es un valor opcional de tipo Hashable asociado a una vista a presentar. nil desactiva el enlace.
struct NextView: View {
  var body: some View {
    ZStack {
      Color.red.opacity(0.7)
      Text("Vista #2")
    }
    .navigationTitle("Segunda vista")
    .navigationSubtitle("Este es el destino de la primera")
  }
}
Enter fullscreen mode Exit fullscreen mode
struct ContentView: View {

  var body: some View {
    NavigationStack {
      ZStack {
        Color.blue.opacity(0.7)
        Text("Vista #1")
      }
      .navigationTitle("Primera vista")
      .navigationSubtitle("Desde acá puedo navegar")
      .toolbar {
        NavigationLink(destination: {
          NextView()
        }) {
          Image(systemName: "paperplane")
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Notar que al presionar el botón del toolbar se efectua la transición. Sin embargo, desde iOS 26 ya no aparece el título de la vista anterior al lado del botón de retroceso.

Creación de un botón de retroceso personalizado

Para empezar, se debe ocultar el botón de retroceso por defecto de la barra de navegación con navigationBarBackButtonHidden(_:).

struct NextView: View {
  var body: some View {
    ZStack {
      Color.red.opacity(0.7)
      Text("Vista #2")
    }
    .navigationTitle("Segunda vista")
    .navigationSubtitle("Este es el destino de la primera")
    .navigationBarBackButtonHidden()
  }
}
Enter fullscreen mode Exit fullscreen mode

Luego, se puede eliminar la vista del stack de navegación por medio de las siguientes variables de ambiente:

  • dismiss. Acción de tipo DismissAction que cierra la vista presentada. La acción se puede ejecutar como un closure.
  • isPresented. Variable de ambiente que indica que la vista se está presentando.
struct NextView: View {
  @Environment(\.dismiss) private var dismiss: DismissAction

  var body: some View {
    ZStack {
      Color.red.opacity(0.7)
      Text("Vista #2")
    }
    .navigationTitle("Segunda vista")
    .navigationSubtitle("Este es el destino de la primera")
    .navigationBarBackButtonHidden()
    .toolbar {
      ToolbarItem(placement: .topBarLeading) {
        Button("Go back", systemImage: "chevron.backward.circle") {
          dismiss()
        }
        .tint(.red)
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Patrón maestro-detalle

En el modo "apilado" del patrón maestro-detalle, se suele tener una lista "maestra" con unos títulos de una colección y, al pulsar alguna de las filas, se navega hacia el "detalle" mostrando la información adicional del elemento.

Esto se puede implementar en SwiftUI con un List, donde cada elemento es un NavigationLink, que generalmente tiene una etiqueta simple (e.g. Text o Label), que tiene como destino la vista detalle que recibe el elemento de la lista seleccionado.

struct DetailView: View {
  let person: Person

  var body: some View {
    VStack {
      Text("Esta es la vista detalle para el siguiente personaje:")
      Text("\(person.name) \(person.lastname)")
    }
    .navigationTitle("Detalle")
    .navigationBarTitleDisplayMode(.inline)
  }
}
Enter fullscreen mode Exit fullscreen mode
struct ContentView: View {

  @State private var searchText: String = ""

  private var filteredPeople: [Person] {
    if searchText.isEmpty {
      people
    } else {
      people.filter {
        $0.name.localizedStandardContains(searchText) }
    }
  }

  var body: some View {
    NavigationStack {
      List(filteredPeople) { person in
        NavigationLink {
          DetailView(person: person)
        } label: {
          //Text("\(person.name) \(person.lastname)")
          Label("\(person.name) \(person.lastname)", systemImage: "person")
        }
      }
      .navigationTitle("Personas")
      .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic), prompt: "¿A quién busca?")
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Manipulando el stack de navegación

Se puede cambiar manualmente la pila de vistas de navegación así:

  1. Al crear un NavigationLink se debe usar el constructor init(_:value:) para señalar cuál es el identificador Hashable asociado a la vista destino deseada. Se debe crear un tipo de dato diferente por cada destino.
  2. Se debe modificar el NavigationStack con navigationDestination(for:destination:) que asocia el tipo de dato de un identificador (data) a una vista de destino (destination). Se pueden suscribir varios navigationDestination a un NavigationStack y entre más alto aparezca uno de ellos en la jerarquía, más prioridad tendrá. Lo anterior implica que si se usan varios valores de un mismo tipo para efectuar una transición, deben ser procesados en el mismo navigationDestination.
  3. Al ejecutar una transición, presionando el NavigationLink, NavigationStack agrega el value a un NavigationPath, que debe ser un Binding (i.e. @State o @Published).

NavigationPath incluye las siguientes propiedades para manejar los valores:

// ⚠️ Notar que Person debe ser Hashable para poder ser usado
// dentro del NavigationPath
struct Person: Identifiable, Hashable {
  let id = UUID()
  let name: String
  let lastname: String
}
Enter fullscreen mode Exit fullscreen mode
// ⚠️ Esta es una vista destino dummy
struct ContactsView: View {
  // ⚠️ Aquí tengo una referencia al navigationPath 
  // del NavigationStack
  @Binding var navigationPath: NavigationPath

  var body: some View {
    ZStack {
      Color.yellow.opacity(0.7)
      Text("Aquí muestro a mis contactos")
    }
    // ⚠️ Oculto el BarBackButton
    .navigationBarBackButtonHidden()
    .toolbar {
      // ⚠️ Agrego un botón de cerrar artesanal
      ToolbarItem(placement: .topBarLeading) {
        Button("Cerrar", systemImage: "x.circle") {
          // ⚠️ .count muestra cuántos elementos hay
          print(navigationPath.count)
          if !navigationPath.isEmpty {
            // ⚠️ .removeLast() hace "pop" del stack
            navigationPath.removeLast()
          }
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
struct DetailView: View {
  let person: Person

  var body: some View {
    VStack {
      Text("Esta es la vista detalle para el siguiente personaje:")
      Text("\(person.name) \(person.lastname)")
    }
    .navigationTitle("Detalle")
    .navigationBarTitleDisplayMode(.inline)
  }
}
Enter fullscreen mode Exit fullscreen mode
struct ContentView: View {

  @State private var searchText: String = ""

  // ⚠️ Se crea un NavigationPath como Binding
  @State private var navigationPath = NavigationPath()

  private var filteredPeople: [Person] {
    if searchText.isEmpty {
      people
    } else {
      people.filter {
        $0.name.localizedStandardContains(searchText) }
    }
  }

  var body: some View {
    // ⚠️ Se debe pasar el navigationPath como Binding al
    // NavigationStack
    NavigationStack(path: $navigationPath) {
      List(filteredPeople) { person in
        NavigationLink(value: person) {
          Label("\(person.name) \(person.lastname)", systemImage: "person")
        }
      }
      .navigationDestination(for: Person.self, destination: { person in
        // ⚠️ Uno de los tipos de valores que se puede procesar
        // es Person, que debe ser Hashable.
        // Una vez se recibe un Person, se pasa como argumento a
        // DetailView
        DetailView(person: person)
      })
      .navigationDestination(for: String.self, destination: { destinationId in
        // ⚠️ Otro de los tipos de valores que se puede procesar
        // es String. Si el String recibido es "Contactos",
        // entonces devuelvo la vista ContactsView(). De lo
        // contrario no devuelvo nada
        if destinationId == "Contactos" {
          // ⚠️ Mando el navigationPath al ContactsView
          ContactsView(navigationPath: $navigationPath)
        }
      })
      .navigationTitle("Personas")
      .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic), prompt: "¿A quién busca?")
      .toolbar {
        // ⚠️ En el toolbar voy a poner un NavigationLink que agrega el valor "Contactos" al path
        NavigationLink(value: "Contactos") {
          Label("Contactos", systemImage: "person")
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode


Bibliografía

Top comments (0)