Skip to content

Instantly share code, notes, and snippets.

@mdb1
Created April 24, 2025 13:07
Show Gist options
  • Select an option

  • Save mdb1/9eb7c3ee9bcad89fb0a83159b9c17b44 to your computer and use it in GitHub Desktop.

Select an option

Save mdb1/9eb7c3ee9bcad89fb0a83159b9c17b44 to your computer and use it in GitHub Desktop.

Revisions

  1. mdb1 created this gist Apr 24, 2025.
    92 changes: 92 additions & 0 deletions NavigationRouter.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,92 @@
    import Observation
    import SwiftUI

    /// A generic navigation router that manages a stack-based navigation.
    ///
    /// `NavigationRouter` is an observable object that tracks a navigation stack using an array of routes.
    /// It provides methods to push, pop, and reset navigation state, allowing for simple navigation flows.
    ///
    /// - Note: The `Route` type must conform to `Hashable`.
    /// - Note: You can see a visual representation of the stack using the `pathDebugDescription` property.
    ///
    /// Usage
    /// ===================================
    /// 1. Create an `enum` with the possible navigation destinations.
    /// 2. Create a `@State` property in the `Router` (or first screen of the flow) for the `NavigationRouter<Route>`.
    /// 3. Add a `NavigationStack(path: $router.navigationPath)`.
    /// 4. Add a `navigationDestination` modifier for the Route enum.
    /// 5. Use the `.environment` modifier to share the `Router` with the child screens.
    /// 6. In the child screens, add `@Environment(NavigationRouter<Route>.self) var router` to use the router
    ///
    /// Example
    /// ===================================
    /// ```swift
    /// struct SampleFlowScreen: View {
    /// @State private var router = NavigationRouter<Route>() // Define the Router
    ///
    /// var body: some View {
    /// NavigationStack(path: $router.navigationPath) {
    /// SampleFlowHomeScreen()
    /// .navigationDestination(for: Route.self) { destination in
    /// switch destination {
    /// case .stepA:
    /// ScreenA()
    /// case .stepB:
    /// ScreenB()
    /// }
    /// }
    /// }
    /// .environment(router) // Share the router with the children screens/views.
    /// }
    /// }
    ///
    /// extension SampleFlowScreen {
    /// enum Route: Hashable {
    /// case stepA, stepB // Define the possible destinations
    /// }
    /// }
    /// ```
    @Observable
    final class NavigationRouter<Route: Hashable> {
    /// The navigation path is the property used in the NavigationStack native component.
    /// We use this array of routes to determine the stack of screens.
    var navigationPath: [Route] = []

    /// Pushes the given route onto the navigationPath.
    func push(_ route: Route) {
    navigationPath.append(route)
    }

    /// Removes the last item from the navigationPath.
    func pop() {
    guard !navigationPath.isEmpty else { return }
    navigationPath.removeLast()
    }

    /// Removes everything from the navigationPath.
    func popToRoot() {
    navigationPath = []
    }

    /// Pops all the elements of the navigationPath up until it finds the given route.
    /// If the given route is not on the array, it is a no-op (nothing happens).
    /// It's recommended to disable or enable the button with this action by checking first if the array contains the element.
    /// Example:
    /// ```swift
    /// Button("Back to step 1") {
    /// router.pop(to: .step1)
    /// }
    /// .disabled(!router.navigationPath.contains(.step1))
    func pop(to route: Route) {
    guard let index = navigationPath.firstIndex(of: route) else { return }
    navigationPath = Array(navigationPath.prefix(through: index))
    }

    /// A string representation of the current navigation stack for debugging purposes.
    ///
    /// Example output: `Navigation Path: [home > detail > settings]`
    var pathDebugDescription: String {
    let pathDescription = navigationPath.map { String(describing: $0) }.joined(separator: " > ")
    return "Navigation Path: [\(pathDescription)]"
    }
    }