The Navigation Challenge in SwiftUI
If you've built anything beyond a simple SwiftUI app, you've likely encountered the navigation puzzle. NavigationStack, sheets, alerts, and popovers — each with their own APIs and state management quirks. What starts as a simple navigation flow quickly becomes a tangled web of @State
variables and scattered navigation logic.
Let me show you a typical SwiftUI navigation scenario:
struct ContentView: View {
@State private var showProfile = false
@State private var showSettings = false
@State private var selectedUserId: String?
@State private var navigationPath = NavigationPath()
@State private var showAlert = false
@State private var alertMessage = ""
var body: some View {
NavigationStack(path: $navigationPath) {
// Your content here
}
.sheet(isPresented: $showProfile) {
if let userId = selectedUserId {
ProfileView(userId: userId)
}
}
.sheet(isPresented: $showSettings) {
SettingsView()
}
.alert("Error", isPresented: $showAlert) {
Button("OK") { }
} message: {
Text(alertMessage)
}
}
}
Sound familiar? This is just the beginning. Now imagine managing this across multiple views, handling deep links, and keeping track of complex navigation flows. It gets messy, fast.
Enter SnazzyRouter
SnazzyRouter is a modern, type-safe routing solution for SwiftUI that brings order to this chaos. It provides a clean, declarative API that consolidates all your navigation needs into a single, manageable system.
📦 Installation
First, add SnazzyRouter to your project via Swift Package Manager:
dependencies: [
.package(url: "https://github.com/DubonYaar/SnazzyRouter.git", from: "1.0.0")
]
🚀 Building Your First Router
Let's build a simple app with proper navigation. The first step is defining your routes:
import SnazzyRouter
import SwiftUI
enum AppRoute: Routable {
case home
case profile(userId: String)
case settings
case articleDetail(articleId: Int, title: String)
@ViewBuilder
var view: some View {
switch self {
case .home:
HomeView()
case .profile(let userId):
ProfileView(userId: userId)
case .settings:
SettingsView()
case .articleDetail(let articleId, let title):
ArticleDetailView(articleId: articleId, title: title)
}
}
}
Notice how clean this is? All your routes in one place, with associated data passed type-safely. No more string-based navigation or lost context.
Setting Up the Router
Now, let's set up our app with the router:
import SwiftUI
import SnazzyRouter
@main
struct MyBlogApp: App {
var body: some Scene {
WindowGroup {
Router(AppRoute.self) { router in
HomeView()
}
}
}
}
That's it! Your app now has a fully functional routing system.
🎯 Navigation in Action
Let's see how navigation works in practice:
struct HomeView: View {
@Environment(RouterState<AppRoute>.self) var router
var body: some View {
ScrollView {
VStack(spacing: 20) {
Text("Welcome to My Blog")
.font(.largeTitle)
.bold()
ForEach(articles) { article in
ArticleCard(article: article)
.onTapGesture {
// Type-safe navigation with parameters
router.push(.articleDetail(
articleId: article.id,
title: article.title
))
}
}
Button("View Profile") {
router.push(.profile(userId: currentUser.id))
}
.buttonStyle(.borderedProminent)
}
.padding()
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
router.push(.settings)
} label: {
Image(systemName: "gear")
}
}
}
}
}
📱 Modal Presentations Made Easy
One of SnazzyRouter's strengths is how it handles different presentation styles:
struct ArticleDetailView: View {
let articleId: Int
let title: String
@Environment(RouterState<AppRoute>.self) var router
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Text(title)
.font(.largeTitle)
.bold()
// Article content...
Button("Share") {
// Present as sheet
router.sheet = RouterModalItem(
destination: .share(articleId: articleId),
dismiss: {
print("Share sheet dismissed")
}
)
}
Button("View Author Profile") {
// Present as full screen cover
router.fullScreenCover = RouterModalItem(
destination: .profile(userId: article.authorId)
)
}
}
.padding()
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Report") {
// Show confirmation dialog
router.showConfirmationDialog(
title: "Report Article",
message: "Why are you reporting this article?",
actions: [
DialogAction(title: "Inappropriate Content") {
reportArticle(reason: .inappropriate)
},
DialogAction(title: "Spam") {
reportArticle(reason: .spam)
},
DialogAction(title: "Cancel", role: .cancel) { }
]
)
}
}
}
}
}
🔥 Advanced Patterns
Deep Linking
SnazzyRouter makes deep linking straightforward:
class AppViewModel: ObservableObject {
let router = RouterState<AppRoute>()
func handleDeepLink(_ url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return }
switch components.path {
case "/article":
if let articleId = components.queryItems?.first(where: { $0.name == "id" })?.value,
let id = Int(articleId) {
router.popToRoot()
router.push(.articleDetail(articleId: id, title: "Article"))
}
case "/profile":
if let userId = components.queryItems?.first(where: { $0.name == "user" })?.value {
router.popToRoot()
router.push(.profile(userId: userId))
}
default:
break
}
}
}
Conditional Navigation
Sometimes you need to check conditions before navigating:
struct PremiumArticleCard: View {
let article: Article
@Environment(RouterState<AppRoute>.self) var router
@EnvironmentObject var userSession: UserSession
var body: some View {
Button {
if userSession.isPremium {
router.push(.articleDetail(articleId: article.id, title: article.title))
} else {
router.showAlert(
Alert(
title: Text("Premium Content"),
message: Text("Upgrade to premium to read this article"),
primaryButton: .default(Text("Upgrade")) {
router.push(.subscription)
},
secondaryButton: .cancel()
)
)
}
} label: {
// Article card UI
}
}
}
Navigation Stack Management
SnazzyRouter gives you full control over your navigation stack:
struct SettingsView: View {
@Environment(RouterState<AppRoute>.self) var router
var body: some View {
List {
Section("Account") {
Button("Sign Out") {
signOut()
// Clear the entire navigation stack
router.popToRoot()
}
}
Section("Debug") {
Button("Current Stack Depth: \(router.path.count)") {
print("Current path: \(router.path)")
}
Button("Go Back 2 Screens") {
if router.path.count >= 2 {
router.path.removeLast(2)
}
}
}
}
}
}
💡 Why SnazzyRouter?
After using SnazzyRouter in production apps, here's what makes it stand out:
1. Type Safety
No more string-based navigation or runtime crashes. Your routes are checked at compile time.
2. Consolidated State
One source of truth for all navigation state — push navigation, modals, alerts, and dialogs.
3. Clean API
The declarative API feels natural in SwiftUI. No more juggling multiple @State
variables.
4. Flexibility
Whether you need simple push navigation or complex modal flows, SnazzyRouter handles it elegantly.
5. Testability
With all navigation logic centralized, testing navigation flows becomes much easier.
🎬 Real-World Example: A Complete Flow
Let's look at a complete user flow to see how it all comes together:
struct CheckoutFlow: View {
@Environment(RouterState<AppRoute>.self) var router
@StateObject private var cart = ShoppingCart()
var body: some View {
VStack {
// Cart items...
Button("Proceed to Checkout") {
if cart.items.isEmpty {
router.showAlert(
Alert(
title: Text("Empty Cart"),
message: Text("Add items to your cart first"),
dismissButton: .default(Text("OK"))
)
)
} else if !userIsSignedIn {
// Show sign-in as modal
router.sheet = RouterModalItem(
destination: .signIn,
dismiss: {
// After sign in, continue to checkout
router.push(.checkout)
}
)
} else {
router.push(.checkout)
}
}
}
}
}
🎯 Conclusion
SwiftUI's navigation APIs are powerful but can quickly become unwieldy in complex apps. SnazzyRouter brings structure and simplicity to navigation, letting you focus on building great user experiences rather than wrestling with navigation state.
The beauty of SnazzyRouter is that it doesn't try to reinvent the wheel — it builds on top of SwiftUI's native navigation while providing a cleaner, more maintainable API. Whether you're building a simple app or a complex navigation flow, SnazzyRouter scales with your needs.
Give it a try in your next SwiftUI project. Your future self (and your team) will thank you for the clean, maintainable navigation code.
Ready to get started? Check out SnazzyRouter on GitHub and make your SwiftUI navigation snazzy!
Have you tried SnazzyRouter in your projects? I'd love to hear about your experience! Drop a comment below or find me on Twitter/X.
DubonYaar
/
SnazzyRouter
A modern, type-safe router for SwiftUI with a clean declarative API
SnazzyRouter ✨
Navigation so smooth, it's got style
A modern, type-safe router for SwiftUI with a clean declarative API
📦 Installation
Swift Package Manager
Add SnazzyRouter to your project through Xcode:
- File → Add Package Dependencies...
- Enter the package URL:
https://github.com/DubonYaar/SnazzyRouter.git
- Click Add Package
Or add it to your Package.swift
:
dependencies: [
.package(url: "https://github.com/DubonYaar/SnazzyRouter.git", from: "1.0.0")
]
🚀 Quick Start
1. Define Your Routes
import SnazzyRouter
import SwiftUI
enum AppRoute: Routable {
case home
case profile(userId: String)
case settings
case detail(item: String)
@ViewBuilder
var view: some View {
switch self {
case .home:
HomeView()
case .profile(let userId):
ProfileView(userId: userId)
case .settings:
SettingsView()
case .detail(let item):
…
Top comments (0)