Skip to content

Instantly share code, notes, and snippets.

@mdb1
Created April 24, 2025 13:07
Show Gist options
  • Save mdb1/9eb7c3ee9bcad89fb0a83159b9c17b44 to your computer and use it in GitHub Desktop.
Save mdb1/9eb7c3ee9bcad89fb0a83159b9c17b44 to your computer and use it in GitHub Desktop.
Simple Navigation Router - SwiftUI
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)]"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment