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/JamesSedlacek/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
}