GoyesDevNavigationSplitView permite encajar dos o tres vistas en una pantalla bajo el patrón...
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 tipo Binding<NavigationSplitViewVisibility>, almacena el estado del NavigationSplitView que puede ser all, doubleColumn o detailOnly. Si solo se quiere mostrar dos vistas en pantalla, se puede omitir el valor content.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.
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:
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),
])
]),
]
Cuando seleccionemos un elemento de la lista del "sidebar" queremos mostrar la vista DetailView, que recibe un Item y pinta:
Item
NavigationLink, que navega hacia una vista minúcula compuesta de un Text que muestra el título del primer hijo del Item seleccionado. Este NavigationLink servirá 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 del NavigationSplitView simplemente se puede borrar el NavigationLink.
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)")
}
}
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")
}
}
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")
Aunque escape 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.
// ⚠️ 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)
}
Un NavigationStack dentro del "sidebar" también puede navegar usando un NavigationLink, pero presentando el contenido en la región del "detail". Esto se logra modificando el NavigationLink con isDetailLink(_:), pasando true como argumento, o simplemente no modificándolo.
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.
// ⚠️ 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")
}
struct ContentView: View {
@State private var selectedId: Item.ID?
@State private var visibility: NavigationSplitViewVisibility = .automatic
@State private var navigationPath = NavigationPath()
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(path: $navigationPath) {
DetailView(item: selected)
}
} else {
Placeholder()
}
}
.onChange(of: selectedId) {
navigationPath = NavigationPath()
}
}
}