NavigationSplitView permite encajar dos o tres vistas en una pantalla bajo el patrón "sidebar"/"content"/"detail". Si no caben, entonces el sistema se encarga de generar la navegación entre pantallas por su cuenta.
-
init(columnVisibility:sidebar:content:detail:).columnVisibility, de tipoBinding<NavigationSplitViewVisibility>, almacena el estado delNavigationSplitViewque puede serall,doubleColumnodetailOnly. Si solo se quiere mostrar dos vistas en pantalla, se puede omitir el valorcontent.
Un NavigationLink en la columna del "sidebar" actualiza el contenido de la columna "content", y un NavigationLink en "content" actualiza la columna "detail".
En este artículo vamos a ver cómo implementar el patrón master/detail con NavigationSplitView usando solo dos columnas.
Los datos
Para empezar, necesitamos una estructura de datos Identifiable que vamos a pintar en la lista de la sección "master", del "sidebar" del NavigationSpliView.
struct Item: Identifiable, Hashable {
let id: UUID = UUID()
let title: String
let children: [Item]?
}
Vamos a crear nuestro dataset. Intencionalmente se hace con tres niveles de jerarquía para que el mismo dataset sirva para el ejemplo de NavigationSplitView con tres columnas:
let items: [Item] = [
.init(title: "Súpergrupo 1", children: [
.init(title: "Grupo 1", children: [
.init(title: "Hoja 1", children: nil),
.init(title: "Hoja 2", children: nil),
.init(title: "Hoja 3", children: nil),
])
]),
.init(title: "Súpergrupo 2", children: [
.init(title: "Grupo 2", children: [
.init(title: "Hoja 4", children: nil),
.init(title: "Hoja 5", children: nil),
.init(title: "Hoja 6", children: nil),
]),
.init(title: "Grupo 3", children: [
.init(title: "Hoja 6", children: nil),
]),
]),
.init(title: "Súpergrupo 3", children: [
.init(title: "Grupo 4", children: [
.init(title: "Hoja 7", children: nil),
]),
.init(title: "Grupo 5", children: [
.init(title: "Hoja 7", children: nil),
.init(title: "Hoja 8", children: nil),
])
]),
]
Vista detalle
Cuando seleccionemos un elemento de la lista del "sidebar" queremos mostrar la vista DetailView, que recibe un Item y pinta:
- Un título general ("Vista detalle")
- El nombre de la clase de tamaño horizontal
horizontalSizeClassactual - "Regular" o "Compact". - El
titledelItem - Un botón, construido con un
NavigationLink, que al ser presionado navega hacia una vista trivial compuesta de unTextque muestra el título del primer hijo delItemseleccionado. EsteNavigationLinkservirá como ejemplo para ilustrar cómo se efectúa una navegación dentro de la vista "detail". Si no se quiere navegar en esta sección delNavigationSplitViewsimplemente se puede borrar elNavigationLink.
NOTA DE VITAL IMPORTANCIA: Si se agrega un NavigationLink SE DEBE envolver con un NavigationStack en algún punto de la jerarquía (esto lo vamos a hacer en ContentView)
struct DetailView: View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
let item: Item
var body: some View {
VStack {
Text("Vista detalle")
.font(.title)
.bold()
Text(horizontalSizeClass == .compact ? "Compacto" : "Regular")
Text(item.title)
NavigationLink("Navegar al primer hijo") {
Text("El primer hijo de \(item.title) es \(item.children?.first?.title ?? "nulo")")
}
}
.navigationTitle("DetailView: \(item.id)")
}
}
Vista de relleno
Cuando no haya ningún elemento seleccionado en el sidebar, vamos a mostrar una vista Placeholder.
struct Placeholder: View {
var body: some View {
Text("Placeholder")
}
}
Asumiendo que DetailView no tiene ningún NavigationLink en su interior, el Placeholder se puede usar de la siguiente manera:
NavigationSplitView {
// ...
} detail: {
if let selectedId, let selected = items.first(where: { $0.id == selectedId }) {
DetailView(item: selected)
} else {
Placeholder()
}
}
En este caso, primero se debe buscar el Item asociado al id seleccionado (selectedId) para poder pintar a DetailView. Si selectedId es nil, entonces se pinta la vista Placeholder.
Navegación dentro del "Detail"
Si se usa un NavigationLink en una vista, se debe envolver con un NavigationStack en algún punto. La implementación más directa sería envolver el DetailView con un NavigationStack dentro del llamado del bloque detail del NavigationSplitView:
NavigationSplitView {
// ...
} detail: {
if let selectedId, let selected = items.first(where: { $0.id == selectedId }) {
NavigationStack {
DetailView(item: selected)
}
} else {
Placeholder()
}
}
En caso de que se necesite controlar el NavigationPath de forma programática, entonces se le puede inyectar uno al NavigationStack sin problema.
Tener en cuenta, al manipular el NavigationPath manualmente, que al cambiar el selectedId muy seguramente sea necesario reiniciar el path. Una solución a este problema podría ser observar cambios sobre selectedId con onChange(of:initial:_:) para re-inicializar la variable navigationPath como se muestra a continuación:
@State private var navigationPath = NavigationPath()
// ...
NavigationSplitView {
// ...
} detail: {
if let selectedId, let selected = items.first(where: { $0.id == selectedId }) {
NavigationStack(path: $navigationPath) {
DetailView(item: selected, path: $navigationPath)
}
} else {
Placeholder()
}
}
.onChange(of: selectedId) {
navigationPath = NavigationPath()
}
NOTA: si no importa manejar el NavigationPath y tampoco se necesita guardar su estado, se puede prescindir de él, y del código agregado para re-inicializarlo.
Sidebar
La barra lateral se puede construir con una lista simple, que almacena un solo valor seleccionado. Tener presente que al pasar selection al List, su tipo de dato debe corresponder al del identificador de los elementos de la lista, en este caso Item.ID. Si solo se quiere seleccionar un elemento, la propiedad será opcional. Si se quisieran seleccionar varios, se usaría Set<Item.ID>. Lo importante a destacar acá es que la selección de elementos de un List se hace con IDENTIFICADORES.
// ...
@State private var selectedId: Item.ID?
// ...
List(items, selection: $selectedId) { supergroup in
Text(supergroup.title)
}
.listStyle(.sidebar) // Esto no es necesario, solo ilustrativo en el Preview
.navigationTitle("Supergroups")
Navegación dentro del mismo sidebar
Aunque lo siguiente escapa de la definición original del patrón master/detail, la vista del sidebar puede navegar hacia otra dentro del mismo sidebar, si se envuelve dentro de un NavigationStack. En este caso ya no importa tanto el selection del List.
Para generar la navegación interna dentro del sidebar se debe usar un NavigationLink y modificarlo con isDetailLink(_:), pasando false como argumento. Recordar que para agregar el NavigationLink debemos envolver todo con un NavigationStack.
// ⚠️ Se envuelve con NavigationStack
NavigationStack {
List(items, selection: $selectedId) { supergroup in
Text(supergroup.title)
}
.listStyle(.sidebar)
.navigationTitle("Supergroups")
NavigationLink("Navegacion interna 1") {
Text("Estoy en otro lado 1")
}
.isDetailLink(false)
NavigationLink("Navegacion interna 2") {
Text("Estoy en otro lado 2")
}
.isDetailLink(false)
}
Al presionar alguno de los NavigationLinks ocurre una navegación dentro del mismo "sidebar".
Navegación manual entre sidebar y detail
Un NavigationStack dentro del "sidebar" también puede navegar a través de un NavigationLink, pero presentando el contenido en la región del "detail". Esto se logra modificando el NavigationLink con isDetailLink(_:), pasando true como argumento. Este es su valor por defecto, así que incluso también se consigue el mismo resultado si no se aplica el modificador.
Por otro lado, es necesario atrapar el enlace de navegación con navigationDestination(for:destination:) sobre el NavigationStack responsable de envolver el NavigationLink en cuestión. Dentro del bloque destination se define la vista destino, como lo haríamos en el bloque detail de NavigationSplitView.
// ⚠️ Se envuelve con NavigationStack
NavigationStack {
// ...
NavigationLink("Navegacion sobre detail 1", value: items.first!.id)
.isDetailLink(true)
}
.navigationDestination(for: UUID.self) { id in
Text("Destino manual de detalle")
}
Montaje de la estructura principal
Vistas las partes anteriores, a continuación se presenta el código completo de ContentView, donde se relacionan el NavigationSplitView el "sidebar" y el "detail":
struct ContentView: View {
@State private var selectedId: Item.ID?
@State private var visibility: NavigationSplitViewVisibility = .automatic
var body: some View {
NavigationSplitView(columnVisibility: $visibility) {
NavigationStack {
List(items, selection: $selectedId) { supergroup in
Text(supergroup.title)
}
.listStyle(.sidebar)
.navigationTitle("Supergroups")
NavigationLink("Navegacion sobre detail 1", value: items.first!.id)
.isDetailLink(true)
NavigationLink("Navegacion interna 1") {
Text("Estoy en otro lado 1")
}
.isDetailLink(false)
NavigationLink("Navegacion interna 2") {
Text("Estoy en otro lado 2")
}
.isDetailLink(false)
}
.navigationDestination(for: UUID.self) { id in
Text("Destino manual de detalle")
}
} detail: {
if let selectedId, let selected = items.first(where: { $0.id == selectedId }) {
NavigationStack {
DetailView(item: selected)
}
} else {
Placeholder()
}
}
}
}
Al seleccionar un elemento de la lista del sidebar, se pinta en el detalle la vista DetailView.
Al presionar el botón de navegación dentro de DetailView se navega a la siguiente vista, dentro de "detail".
Si se presiona uno de los NavigationLinks internos del "sidebar", ocurre una navegación interna dentro del "sidebar":









Top comments (0)