0

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.

  1. 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.
  2. Watching the memory usage after adding many views and then popping them all, it seems higher. Are there any real memory leaks with this?
  3. 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.
  4. Have I missed some obvious way of achieving this extensible architecture using built-in capabilities?
Scott McKenzie
  • 16,052
  • 8
  • 45
  • 70

0 Answers0