A lightweight navigation system for SwiftUI applications that provides programmatic navigation with hierarchical state management.
- Type-Safe Navigation: Strongly-typed routing with compile-time safety
- Layered Navigation: Present views on top of each other, modals within modals, all managed automatically
- Modal Presentations: Built-in support for sheets and full-screen covers
- Programmatic Control: Push, pop, present, and dismiss from anywhere
- Debug Support: Comprehensive hierarchy logging for development
- Reactive Updates: Combine publishers for navigation state changes
- Animation Control: Optional animation for all navigation actions
- iOS 16.0+
- Swift 5.9+
- Xcode 15.0+
Add NavigationKit to your project using Swift Package Manager:
dependencies: [
.package(url: "https://github.com/ahmedelmoughazy/NavigationKit.git", from: "0.2.0")
]Then add it to your target dependencies:
.target(
name: "YourTarget",
dependencies: ["NavigationKit"]
)Mark your views with the @Routable macro:
import SwiftUI
import NavigationKit
@Routable
struct ProfileView: View {
let userId: String
var body: some View {
Text("Profile for user: \(userId)")
}
}
@Routable
struct SettingsView: View {
var body: some View {
Text("Settings")
}
}Create a router and wrap your root view with BaseNavigation:
import SwiftUI
import NavigationKit
@main
struct MyApp: App {
private let router = Router()
var body: some Scene {
WindowGroup {
BaseNavigation(router: router) {
RootView()
}
}
}
}Access the router from any view using @EnvironmentObject and navigate programmatically:
struct RootView: View {
@EnvironmentObject var router: Router
var body: some View {
VStack {
Button("Go to Profile") {
router.push(destination: ProfileView(userId: "123"))
}
Button("Open Settings as Sheet") {
router.present(destination: SettingsView(), as: .sheet)
}
}
}
}// Push a new destination
router.push(destination: ProfileView(userId: "123"))
// Pop the current view
router.pop()
// Pop to a specific destination
router.pop(to: HomeView())
// Pop to presentation (clear current stack)
router.popToPresentation()
// Pop all and dismiss all modals
router.popAll()// Present as sheet
router.present(destination: SettingsView(), as: .sheet)
// Present as full-screen cover
router.present(destination: DetailView(), as: .fullScreenCover)
// Dismiss current modal
router.dismiss()// Insert destination at specific index
router.insert(destination: HomeView(), at: 0)
// Remove specific destinations
router.remove(destinations: ProfileView(userId: "123"))
// Replace entire navigation path
router.applyPath([HomeView(), ProfileView(userId: "456")])All navigation methods support optional animation control:
router.push(destination: .profileView(userId: "123"), animated: false)
router.pop(animated: false)
router.present(destination: .settingsView, as: .sheet, animated: false)Monitor navigation state changes reactively:
// Get current route
let currentRoute: [String] = router.currentRoute
// Subscribe to route changes
router.currentRoutePublisher
.sink { route in
print("Navigation changed: \(route)")
}
.store(in: &cancellables)NavigationKit includes a built-in alert system that works seamlessly with modal presentations.
When using standard SwiftUI alert modifiers alongside sheet or full-screen cover presentations, showing an alert can cause the modal presentation to be dismissed unexpectedly. This is a known SwiftUI behavior where alert presentation can interfere with modal lifecycle management.
NavigationKit's alert system solves this problem by:
- Modal-Safe Presentation: Alerts won't dismiss sheets or full-screen covers
- State Management: Properly integrated with the navigation hierarchy
- Alert Replacement: Seamlessly replace one alert with another
- Automatic Cleanup: Properly synchronizes state with the router
Present alerts through the router's alertItem property:
struct MyView: View {
@EnvironmentObject var router: Router<Route>
var body: some View {
Button("Show Alert") {
router.presentAlert(alertItem: AlertItem(
title: "Confirmation",
message: "Are you sure?",
actionButtons: [
AlertActionButton(title: "Confirm", style: .primary) {
// Handle confirmation
},
AlertActionButton(title: "Cancel", style: .cancel)
]
)
)
}
}
}NavigationKit provides four button styles to match your alert's intent:
.primary: The main action (emphasized with keyboard shortcut).secondary: Alternative actions (default style).destructive: Dangerous actions like delete.cancel: Dismisses without action
Alerts are automatically dismissed when:
- The user taps any action button.
- You call
router.dismissAlert().
NavigationKit provides powerful logging capabilities to help debug navigation flows.
Set the logging style when creating the router:
// Disable logging (default)
let router = Router(loggingStyle: .disabled)
// Enable hierarchical logging (tree view)
let router = Router(loggingStyle: .hierarchical)
// Enable flat logging (array view)
let router = Router(loggingStyle: .flat)When logging is enabled, the navigation hierarchy is automatically printed whenever navigation state changes.
Disabled (default) - No logging output
Hierarchical - Tree view with indentation:
π― Router#a1b2c
π± Path: [home, profile]
π Sheet: settings
βββ π― Router#d3e4f
π± Path: [details]
Flat - Array view with sequential listing:
Routers: [
π― Router#a1b2c | π± Path: [home, profile] | π Sheet: settings
π― Router#d3e4f | π± Path: [details]
]
You can change the logging style at any time:
// Switch to hierarchical logging
router.loggingStyle = .hierarchical
// Disable logging
router.loggingStyle = .disabledYou can also manually trigger logging at any time:
router.debugPrintCompleteHierarchy()Contributions are welcome! Please feel free to submit a Pull Request.
NavigationKit is available under the MIT license. See the LICENSE file for more info.
Ahmed Elmoughazy - @ahmedelmoughazy
Built with Swift Macros and SwiftSyntax for powerful compile-time code generation.
