| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
A modern, type-safe navigation library built entirely on SwiftUI's NavigationStack. NavigationPilot gives you a clean, router-style API — push, pop, popTo, replace — without UIKit, without type erasure, and without writing the same boilerplate across every project.
I have also written a detailed article on NavigationPilot explaining the design decisions and every pattern in depth. You can read it here: Type-Safe SwiftUI Navigation: Building a Better NavigationStack with NavigationPilot
| Ex1 — Push / Pop | Ex2 — Parameters | Ex3 — Callback | Ex4 — Split Screen |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
In Xcode: File → Add Package Dependencies and enter:
https://github.com/DKVekariya/NavigationPilot
Or add it directly to your Package.swift:
dependencies: [
.package(url: "https://github.com/DKVekariya/NavigationPilot", from: "1.0.0")
]NavigationPilot/ ├── Sources/NavPilot/ │ ├── NavPilot.swift # Observable router — push, pop, replace │ └── NavPilotHost.swift # Root view + NavigationStack binding ├── Examples/NavPilotDemo/ │ └── NavPilotDemo/ │ ├── ContentView.swift # Switch between the demo scenarios │ ├── E1SimplePush.swift # push / pop / popTo / popToRoot │ ├── E2PushWithData.swift # Typed struct params, replace │ ├── E3CloserPass.swift # Closure as route parameter │ └── E4SplitScreen.swift # Two independent pilots, vertical split ├── Tests/NavPilotTests/ │ └── NavPilotTests.swift # Core router behavior tests ├── Package.swift └── README.md
// Every screen is a case. Parameters are typed — the compiler
// will reject the wrong type at the call site.
enum AppRoute: Hashable {
case home
case detail(id: Int)
case settings
}@main
struct MyApp: App {
@StateObject var pilot = NavPilot(initial: AppRoute.home, persistState: true)
var body: some Scene {
WindowGroup {
NavPilotHost(pilot) { route in
switch route {
case .home: HomeView()
case .detail(let id): DetailView(id: id)
case .settings: SettingsView()
}
}
}
}
}// The pilot is injected automatically — no passing through init.
struct HomeView: View {
@EnvironmentObject var pilot: NavPilot<AppRoute>
var body: some View {
VStack {
Button("Open Detail") { pilot.push(.detail(id: 42)) }
Button("Settings") { pilot.push(.settings) }
}
.navigationTitle("Home")
}
}Carry closures as route parameters to fully decouple destination views from navigation logic:
enum AppRoute: Hashable {
case profile(onSignOut: () -> Void)
// Implement Hashable manually for closure-carrying cases
func hash(into hasher: inout Hasher) { hasher.combine("profile") }
static func == (lhs: AppRoute, rhs: AppRoute) -> Bool { true }
}
// SignInView defines the navigation intent at the push call site
pilot.push(.profile(onSignOut: {
pilot.popTo(.home)
}))
// ProfileView has zero knowledge of NavPilot
struct ProfileView: View {
let onSignOut: () -> Void
var body: some View {
Button("Sign Out", role: .destructive) { onSignOut() }
}
}Perfect for post-action flows where the back-stack should differ from the forward path:
// Before: [home → cart → checkout]
// After: [home → confirmation]
// Swipe-back from confirmation now goes to home, not checkout.
pilot.replace([.home, .confirmation])For Codable routes, you can generate a deep-link URL for the current stack and restore it later:
let url = pilot.deepLinkURL()
pilot.handleDeepLink(url)When you want the stack to survive app relaunches, opt in with persistState: true:
@StateObject var pilot = NavPilot(initial: AppRoute.home, persistState: true)| Method | Description |
|---|---|
| push(_ route) | Push one route onto the stack |
| push(_ routes...) | Push multiple routes in one call |
| pop() | Pop the top route. No-op at root |
| pop(count: n) | Pop n routes at once, clamped to root |
| popTo(_ route) | Pop back to the first occurrence of a route |
| popToLast(_ route) | Pop back to the last occurrence of a route |
| popToRoot() | Clear everything above the root |
| replace(_ routes) | Replace the entire stack |
| replaceCurrent(with:) | Swap only the top-most route |
| deepLinkURL() | Generate a URL that represents the current stack |
| handleDeepLink(_:) | Restore the stack from a deep-link URL |
| persistState | Opt-in initializer behavior for Codable routes that stores the stack |
| Property | Type | Description |
|---|---|---|
| stack | [T] | Read-only live stack. Index 0 is always root |
| current | T? | Route at the top of the stack |
| depth | Int | Number of screens in the stack |
Open Examples/NavPilotDemo/NavPilotDemo/ContentView.swift and change navType to switch between examples:
// ▼ Change this number to switch examples ▼
private let activeExample: Int = 1| # | Example | Demonstrates |
|---|---|---|
| 1 | Simple Push / Pop | push, pop, popTo, popToRoot |
| 2 | Typesafe Parameters | Typed struct params, replace, replaceCurrent |
| 3 | Callback Route | Closure passed as a route parameter |
| 4 | Split Screen | Two independent pilots in a vertical split layout |
| 5 | Deep Linking | Generate and restore a stack from a URL |
| 6 | State Persistence | Save and restore stack across app relaunch |
| 7 | Stack Inspector | Show the live route stack as an overlay |
Divyesh Vekariya
MIT. See LICENSE for details.
Built with ❤️ using SwiftUI
Last Updated: April 2026
| Back | FazBrowse Home | New Git URL |