SwiftUI-Navigation is a lightweight SwiftUI navigation library.
- Leverages 1st-party APIs
NavigationStack&NavigationDestination. - Never be confused about
NavigationLinkorNavigationPathagain! (You don't need them) - Type-Safe Navigation (better performance than type-erasing).
- Centralized Navigation Logic.
- Dynamic Navigation Stack Management.
- Unit Tested protocol implementations.
- Zero 3rd party dependencies.
| Platform | Minimum Version |
|---|---|
| iOS | 16.0 |
| macOS | 13.0 |
| tvOS | 16.0 |
| watchOS | 9.0 |
You can install SwiftUI-Navigation using the Swift Package Manager.
- In Xcode, select
File>Add Package Dependencies.
- Copy & paste the following into the
Search or Enter Package URLsearch bar.
https://github.com/Sedlacek-Solutions/SwiftUI-Navigation.git - Xcode will fetch the repository & the
SwiftUI-Navigationlibrary will be added to your project.
- Create a
Destinationenum that conforms to theDestinationprotocol.
import Navigation import SwiftUI enum ExampleDestination: Destination { case detail case settings var body: some View { switch self { case .detail: DetailView() case .settings: SettingsView() } } }- Create a
DestinationStateobject and wrap yourRootViewwith aNavigator.
import SwiftUI import Navigation struct ContentView: View { @DestinationState private var destinations: [ExampleDestination] = [] var body: some View { Navigator(path: $destinations) { Button("Go to Settings") { destinations.navigate(to: .settings) } } } }- Handle navigation using the
DestinationStatefunctions
/// Navigate back in the stack by a specified count. func navigateBack(_ count: Int) /// Navigate back to a specific destination in the stack. func navigateBack(to destination: Destination) /// Navigate to the root of the stack by emptying it. func navigateToRoot() /// Navigate to a specific destination by appending it to the stack. func navigate(to destination: Destination) /// Navigate to multiple destinations by appending them to the stack. func navigate(to destinations: [Destination]) /// Replace the current stack with new destinations. func replace(with destinations: [Destination])import Navigation import SwiftUI enum ContentDestination: Destination { case detail(Color) case settings var body: some View { switch self { case .detail(let color): ColorDetail(color: color) case .settings: SettingsView() } } } struct ContentView: View { @DestinationState private var destinations: [ContentDestination] = [] private let colors: [Color] = [.red, .green, .blue] var body: some View { Navigator(path: $destinations) { List(colors, id: \.self) { color in color .onTapGesture { destinations.navigate(to: .detail(color)) } } } } } struct ColorDetail: View { private let color: Color init(color: Color) { self.color = color } var body: some View { color.frame(maxWidth: .infinity, maxHeight: .infinity) } }SwiftUI-Navigation provides several View extensions to simplify common navigation and presentation patterns when working with Destination types.
This extension is a convenience wrapper around the standard SwiftUI navigationDestination(for:destination:) modifier. It's tailored for use with types conforming to Destination, automatically using the Destination instance itself as the destination view.
// Usage within a view: // SomeView().navigationDestination(for: MyDestination.self) // This is often handled automatically by Navigator.Navigator uses this extension internally to set up navigation for your Destination enum.
Presents a sheet when a binding to an optional Destination & Identifiable item becomes non-nil. The content of the sheet is the Destination item itself.
item: ABindingto an optionalDestination & Identifiableitem.onDismiss: An optional closure executed when the sheet dismisses.
Note: The Destination type used with this modifier must also conform to Identifiable.
import SwiftUI import Navigation // Ensure your Destination enum conforms to Identifiable. // For enums with associated values, you might need to add an explicit `id`. enum ModalDestination: Destination, Identifiable { case helpPage case userDetails(id: String) // Example of making it Identifiable var id: String { switch self { case .helpPage: return "helpPage" case .userDetails(let id): return "userDetails-\(id)" } } var body: some View { switch self { case .helpPage: HelpView() // Placeholder case .userDetails(let id): UserDetailsView(userID: id) // Placeholder } } } struct MyContentView: View { @State private var sheetItem: ModalDestination? var body: some View { Button("Show Help Sheet") { sheetItem = .helpPage } .sheet(item: $sheetItem) } } // Placeholder Views for example struct HelpView: View { var body: some View { Text("Help Information") } } struct UserDetailsView: View { let userID: String var body: some View { Text("Details for user \(userID)") } }Available on iOS 17.0+, macOS 14.0+, tvOS 17.0+, watchOS 10.0+.
Presents a view using navigationDestination(item:destination:) when a binding to an optional Destination item becomes non-nil. The destination view is the Destination item itself. This is useful for modal-style presentations or alternative navigation flows that don't necessarily push onto the main NavigationStack.
item: ABindingto an optionalDestinationitem.
import SwiftUI import Navigation // Assuming MyDetailDestination is a Destination enum // enum MyDetailDestination: Destination { case info, settings ... } @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) struct AnotherScreen: View { @State private var presentedDetail: MyDetailDestination? // MyDetailDestination conforms to Destination var body: some View { Button("Show Info Modally") { presentedDetail = .info // Assuming .info is a case in MyDetailDestination } .navigationDestination(item: $presentedDetail) } }The Navigator essentially wraps your view with a NavigationStack. It uses the navigationDestination(for: DestinationType.self) view extension (detailed in the "View Extensions" section) to automatically handle presenting the views associated with your Destination types.
// Simplified structure of Navigator's body: NavigationStack(path: $path) { // $path is your @DestinationState's binding rootContent() .navigationDestination(for: DestinationType.self) // Uses the Destination-specific extension }