|
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)]" |
|
} |
|
} |