A generic, reusable navigation router for SwiftUI applications. Supports both simple single-stack navigation and complex tab-based navigation with independent navigation stacks and sheet presentation.
- 🎯 Simple Router -
SimpleRouterfor single NavigationStack apps - 🏷️ Tab Router -
Routerfor tab-based apps with independent navigation per tab - đź“„ Sheet Management - Built-in sheet presentation and dismissal
- 🔄 SwiftUI Integration - Uses
@Observablefor reactive state updates - đź§µ Thread Safe -
@MainActorimplementation ensures UI safety - 📱 iOS 17+ Ready - Built for modern SwiftUI patterns
Add this package to your project:
dependencies: [
.package(url: "https://github.com/dimillian/AppRouter.git", from: "1.0.0")
]AppRouter provides two routers depending on your app's navigation needs:
SimpleRouter- For apps with a single NavigationStackRouter- For apps with tab-based navigation
Perfect for apps that don't use tabs and just need a single navigation stack with sheet support.
import SwiftUI
import AppRouter
// 1. Define your destination and sheet types
enum Destination: DestinationType {
case detail(id: String)
case settings
case profile(userId: String)
}
enum Sheet: SheetType {
case compose
case settings
var id: Int { hashValue }
}
// 2. Use SimpleRouter
struct ContentView: View {
@State private var router = SimpleRouter<Destination, Sheet>()
var body: some View {
NavigationStack(path: $router.path) {
HomeView()
.navigationDestination(for: Destination.self) { destination in
destinationView(for: destination)
}
}
.sheet(item: $router.presentedSheet) { sheet in
sheetView(for: sheet)
}
.environment(router)
}
@ViewBuilder
private func destinationView(for destination: Destination) -> some View {
switch destination {
case .detail(let id):
DetailView(id: id)
case .settings:
SettingsView()
case .profile(let userId):
ProfileView(userId: userId)
}
}
@ViewBuilder
private func sheetView(for sheet: Sheet) -> some View {
switch sheet {
case .compose:
ComposeView()
case .settings:
SettingsSheet()
}
}
}
// 3. Navigate from anywhere in your app
struct HomeView: View {
@Environment(SimpleRouter<Destination, Sheet>.self) private var router
var body: some View {
VStack {
Button("Go to Detail") {
router.navigateTo(.detail(id: "123"))
}
Button("Show Compose Sheet") {
router.presentSheet(.compose)
}
}
}
}For apps that use TabView with independent navigation stacks per tab.
import AppRouter
enum AppTab: String, TabType, CaseIterable {
case home, profile, settings
var id: String { rawValue }
var icon: String {
switch self {
case .home: return "house"
case .profile: return "person"
case .settings: return "gear"
}
}
}enum Destination: DestinationType {
case detail(id: String)
case list
case profile(userId: String)
}
enum Sheet: SheetType {
case settings
case profile
case compose
var id: Int { hashValue }
}import SwiftUI
import AppRouter
struct ContentView: View {
@State private var router = Router<AppTab, Destination, Sheet>(initialTab: .home)
var body: some View {
TabView(selection: $router.selectedTab) {
ForEach(AppTab.allCases) { tab in
NavigationStack(path: $router[tab]) {
HomeView()
.navigationDestination(for: Destination.self) { destination in
destinationView(for: destination)
}
}
.tabItem {
Label(tab.rawValue.capitalized, systemImage: tab.icon)
}
.tag(tab)
}
}
.sheet(item: $router.presentedSheet) { sheet in
sheetView(for: sheet)
}
}
@ViewBuilder
private func destinationView(for destination: Destination) -> some View {
switch destination {
case .detail(let id):
DetailView(id: id)
case .list:
ListView()
case .profile(let userId):
ProfileView(userId: userId)
}
}
@ViewBuilder
private func sheetView(for sheet: Sheet) -> some View {
switch sheet {
case .settings:
SettingsView()
case .profile:
ProfileSheet()
case .compose:
ComposeView()
}
}
}For single NavigationStack apps:
@Observable @MainActor
public final class SimpleRouter<Destination: DestinationType, Sheet: SheetType>path: [Destination]- Navigation pathpresentedSheet: Sheet?- Currently presented sheet
navigateTo(_:)- Navigate to a destinationpopNavigation()- Pop last destination from stackpopToRoot()- Clear navigation stackpresentSheet(_:)- Present a sheetdismissSheet()- Dismiss current sheet
For tab-based apps with independent navigation per tab:
@Observable @MainActor
public final class Router<Tab: TabType, Destination: DestinationType, Sheet: SheetType>selectedTab: Tab- Currently selected tabpresentedSheet: Sheet?- Currently presented sheetselectedTabPath: [Destination]- Navigation path for current tab
navigateTo(_:for:)- Navigate to a destinationpopNavigation(for:)- Pop last destination from stackpopToRoot(for:)- Clear navigation stack for tabpresentSheet(_:)- Present a sheetdismissSheet()- Dismiss current sheet
public protocol DestinationType: Hashable {}public protocol SheetType: Hashable, Identifiable {}public protocol TabType: Hashable, CaseIterable, Identifiable, Sendable {
var icon: String { get }
}Only needed for tab-based navigation
To avoid verbose generic syntax throughout your app, create a type alias:
// Define once in your app
typealias AppRouter = Router<AppTab, Destination, Sheet>
typealias AppSimpleRouter = SimpleRouter<Destination, Sheet>
// Then use the cleaner syntax everywhere
@Environment(AppRouter.self) private var router
@State private var router = AppRouter(initialTab: .home)struct HomeView: View {
@Environment(AppRouter.self) private var router
var body: some View {
VStack {
Button("Go to Detail") {
router.navigateTo(.detail(id: "123"))
}
Button("Show Settings") {
router.presentSheet(.settings)
}
Button("Go to Profile Tab") {
router.selectedTab = .profile
router.navigateTo(.profile(userId: "user123"), for: .profile)
}
}
}
}struct App: View {
@State private var router = Router<AppTab, Destination, Sheet>(initialTab: .home)
var body: some View {
ContentView()
.environment(router)
.environment(\.currentTab, router.selectedTab)
}
}- iOS 17.0+
- macOS 14.0+
- tvOS 17.0+
- watchOS 10.0+
- Swift 5.9+
MIT License - see LICENSE file for details