I'm trying to design an extensible navigation system using NavigationSplitView
, NavigationStack
, and NavigationPath
.
Definition of a route:
struct Route: Identifiable, Hashable {
let id: String
let hashValue: Int
let title: String
let content: () -> AnyView
init<Content: View>(title: String, content: @escaping () -> Content) {
self.title = title
self.id = title
self.hashValue = UUID().hashValue
self.content = { AnyView(content()) }
}
func hash(into hasher: inout Hasher) {
hasher.combine(hashValue)
}
static func == (lhs: Route, rhs: Route) -> Bool {
lhs.hashValue == rhs.hashValue
}
}
NavigationPath wrapper.
final class Navigation: ObservableObject {
@Published var path = NavigationPath()
func push(_ route: Route) {
path.append(route)
}
func pop() {
path.removeLast(1)
}
func root() {
path.removeLast(path.count)
}
func root(_ route: Route) {
path.removeLast(path.count)
path.append(route)
}
}
With all the heavy lifting happening in the split and stack views.
struct ContentView: View {
@ObservedObject var navigation: Navigation = .init()
@ObservedObject var routes: Routes = .init()
var body: some View {
NavigationSplitView {
sidebar
} detail: {
detail
}
}
private var sidebar: some View {
NavigationStack {
List {
ForEach(routes.items) { item in
Button {
navigation.root()
} label: {
Text("Home")
}
Button {
navigation.root(item)
} label: {
Text(item.title)
}
}
}
}
}
private var detail: some View {
NavigationStack(path: $navigation.path) {
Group {
Text("Root View")
}
.navigationDestination(for: Route.self, destination: { route in
route.content()
})
}
}
}
This all seems to work reasonably well, but I have some questions.
- When I pop all the views on iOS I end up in the root view. Is this correct or typical behaviour? It doesn't look so bad on the iPad, but feels odd on iOS.
- Watching the memory usage after adding many views and then popping them all, it seems higher. Are there any real memory leaks with this?
- Is there a better way of wrapping a view such that it's not created when the menu is created but provides the same flexibility. My approach uses a closure and
AnyView
and I'm not sure if this will cause problems. - Have I missed some obvious way of achieving this extensible architecture using built-in capabilities?