Best Practices for Navigation in SwiftUI: Pushing a View onto the NavigationStack
Navigation is a core part of any SwiftUI app. Handling it efficiently improves both the **developer experience** and the **user experience**. In this post, we’ll explore best practices for pushing views onto a NavigationStack
.
The Problem with Traditional Navigation
A common way developers push views in SwiftUI is by using NavigationLink
like this:
NavigationLink("Go to Account", destination: AccountScreen())
While this works, it has drawbacks:
- Hardcoded Destinations: Managing multiple navigation paths becomes difficult.
- No Type Safety: It's easy to introduce inconsistencies across the app.
- Difficult to Centralize Logic: Navigation is scattered across different views.
A Protocol-Driven Approach to Navigation
Using a protocol for defining routes allows for:
- Centralized navigation logic
- Type safety (reducing runtime errors)
- Easier testing and maintainability
Defining a Routable Protocol
public protocol Routable: Hashable, View, Identifiable {
var id: Self { get }
static func destination(for route: Self) -> Body
}
public extension Routable {
var id: Self { self }
static func destination(for route: Self) -> Body {
route.body
}
func hash(into hasher: inout Hasher) {
hasher.combine(self)
}
}
Using Routable
with NavigationStack
Step 1: Define Routes
enum SettingsRoute: Routable {
case account
case makeSuggestion
case reportIssue
var body: some View {
switch self {
case .account: AccountScreen()
case .makeSuggestion: MakeSuggestionScreen()
case .reportIssue: ReportIssueScreen()
}
}
}
Step 2: Manage Navigation with State
@MainActor
struct SettingsScreen {
@State var routeToPush: SettingsRoute? = nil
func accountAction() {
routeToPush = .account
}
}
Step 3: Implement the View with NavigationStack
extension SettingsScreen: View {
var body: some View {
NavigationStack {
VStack {
Button(
"Go to Account",
action: accountAction
)
}
.navigationDestination(
item: $routeToPush,
destination: SettingsRoute.destination
)
}
}
}
Why This Approach Works Well
- Centralized Navigation Logic: All routes are managed in a single
enum
. - Type Safety: The compiler ensures correctness.
- Improved Readability: No scattered
NavigationLink
declarations. - Better Maintainability: Decouples navigation from UI logic.
Next Up: Presenting Sheets
In the next post, we’ll explore **best practices for presenting modals (sheets) in SwiftUI**, extending our Routable
pattern.