SwiftUINavigation
Framework for Implementing Clean Navigation in SwiftUI
Install / Use
/learn @RobertDresler/SwiftUINavigationREADME
SwiftUINavigation
Framework for Implementing Clean Navigation in SwiftUI
![]()
If anything is unclear, feel free to reach out! I'm happy to clarify or update the documentation to make things more straightforward. 🚀
If you find this repository helpful, feel free to give it a ⭐ or share it with your colleagues 👩💻👨💻 to help grow the community of developers using this framework!
Features
- ✅ Handles both simple concepts, like presenting/stack navigation, and complex concepts, such as content-driven (deep linking) and step-by-step navigation (See Examples app)
- ✅ Built on native SwiftUI components, leveraging state-driven architecture in the background
- ✅ Clearly separates the navigation and presentation layers
- ✅ Compatible with modular architecture
- ✅ Perfect for everything from simple apps to large-scale projects
- ✅ You can choose any architecture that fits your needs—MV, MVVM, or even TCA
- ✅ Fully customizable and extendable to fit your specific needs
- ✅ Inspired by the well-known Coordinator pattern but without the hassle of manually managing parent-child relationships
- ✅ Supports iOS 16 and later
- ✅ Supports custom
fullScreenCovertransitions (e.g.,scale,opacity) from iOS 17 and stack transitions (e.g.,zoom) from iOS 18 - ✅ Supports iPadOS 16, macOS 13, and Mac Catalyst 16 as well – optimized for multi-window experiences
- ✅ Enables calling environment actions, such as
requestReview - ✅ Supports backward compatibility with UIKit via
UIViewControllerRepresentable– easily presentSFSafariViewControllerorUIActivityViewController - ✅ Supports Swift 6 and is concurrency safe


Core Idea - NavigationModel
In SwiftUI, State/Model/ViewModel serves as the single source of truth for the view's content. This framework separates the state of navigation into a dedicated model called NavigationModel.
Think of it as a screen/module or what you might recognize as a coordinator or router. These NavigationModels form a navigation graph, where each NavigationModel maintains its own state using @Published properties. This state is rendered using native SwiftUI mechanisms, and when the state changes, navigation occurs.
For example, when you update presentedModel, the corresponding view for the new presentedModel is presented. The NavigationModel is also responsible for providing the screen's content within its body, which is then integrated into the view hierarchy by the framework.
Below is a diagram illustrating the relationships between components when using SwiftUINavigation alongside MVVM or MV architecture patterns:

NavigationCommand represents an operation that modifies the navigation state of NavigationModel. For example, a PresentNavigationCommand sets the presentedModel. These operations can include actions like .stackAppend(_:animated:) (push), .stackDropLast(_:animated:) (pop), .present(_:animated:), .dismiss(animated:), .openURL(_) and more.
To get started, I recommend exploring the Examples app to get a feel for the framework. Afterward, you can dive deeper on your own. For more detailed information, check out the Documentation.
Getting Started
I highly recommend starting by exploring the Examples app. The app features many commands that you can use to handle navigation, as well as showcases common flows found in many apps. It includes everything from easy login/logout flows to custom navigation bars with multiple windows.
If you prefer to explore the framework on your own, check out Explore on Your Own and the Documentation.
Explore Examples App
<details> <summary>Click to see details 👈</summary>Read This First
- The app is modularized using SPM to demonstrate its compatibility with a modular architecture. However, when integrating it into your app, you can keep everything in a single module if you prefer.
- Some modules follow the MV architecture, while others with a ViewModel use MVVM. The choice of architecture is entirely up to you—SwiftUINavigation solely provides a solution for the navigation layer.
- Dependencies for
NavigationModels are handled via initializers. To avoid passing them in every init, you can use a dependency manager like swift-dependencies. - There is a
Sharedmodule that contains e.g. objects for deep linking, which can be used across any module. Implementations of certain services are located in the main app within theDependenciesfolder. - The
ActionableListmodule serves as a generic module for list screens with items. To see what items each list contains, check the implementation of factories in the module’sData/Factories/...folder.
Installation
-
Get the repo
- Clone the repo:
git clone https://github.com/RobertDresler/SwiftUINavigation - Download the repo (don't forget to rename the downloaded folder to
SwiftUINavigation)
- Clone the repo:
-
Open the app at path
SwiftUINavigation/Examples.xcodeproj -
Run the app
- On simulator
- On a real device (set your development team)
-
(optional) The app might fail to build if SwiftUINavigation’s macros aren’t enabled. In some cases, you may need to explicitly enable them. In Xcode, simply click on the error message and choose Trust & Enable to resolve it.
-
Explore the app
Explore on Your Own
<details> <summary>Click to see details 👈</summary>- To get started, first add the package to your project:
- In Xcode, add the package by using this URL:
https://github.com/RobertDresler/SwiftUINavigationand choose the dependency rule up to next major version from2.2.2 - Alternatively, add it to your
Package.swiftfile:.package(url: "https://github.com/RobertDresler/SwiftUINavigation", from: "2.2.2")
- (optional) The app or package might fail to build if SwiftUINavigation’s macros aren’t enabled. In some cases, you may need to explicitly enable them. In Xcode, simply click on the error message and choose Trust & Enable to resolve it.
Once the package is added, you can copy this code and begin exploring the framework by yourself:
MV
<details> <summary>Click to view the example code 👈</summary>import SwiftUI
import SwiftUINavigation
@main
struct YourApp: App {
@StateObject private var rootNavigationModel = DefaultStackRootNavigationModel(
HomeNavigationModel()
)
var body: some Scene {
WindowGroup {
RootNavigationView(rootModel: rootNavigationModel)
}
}
}
@NavigationModel
final class HomeNavigationModel {
var body: some View {
HomeView()
}
func showDetail(onRemoval: @escaping () -> Void) {
let detailNavigationModel = DetailNavigationModel()
.onMessageReceived { message in
switch message {
case _ as FinishedNavigationMessage:
onRemoval()
default:
break
}
}
execute(.present(.sheet(.stacked(detailNavigationModel))))
}
}
struct HomeView: View {
@EnvironmentNavigationModel private var navigationModel: HomeNavigationModel
@State private var dismissalCount = 0
var body: some View {
VStack {
Text("Hello, World from Home!")
Text("Detail dismissal count: \(dismissalCount)")
Button(action: { showDetail() }) {
Text("Go to Detail")
}
}
}
func showDetail() {
navigationModel.showDetail(onRemoval: { dismissalCount += 1 })
}
}
@NavigationModel
final class DetailNavigationModel {
var body: some View {
DetailView()
}
}
struct DetailView: View {
@EnvironmentNavigationModel private var navigationModel: DetailNavigationModel
var body: some View {
Text("Hello world from Detail!")
}
}
</details>
MVVM
<details> <summary>Click to view the example code 👈</summary>import SwiftUI
import SwiftUINavigation
@main
struct YourApp: App {
@StateObject private var rootNavigationModel = DefaultStackRootNavigationModel(
HomeNavigationModel()
)
var body: some Scene {
WindowGroup {
RootNavigationView(rootModel: rootNavigationModel)
}
}
}
@NavigationModel
final class HomeNavigationModel {
private lazy var viewModel = HomeViewModel(navigationModel: self)
var body: some View {
HomeView(viewModel: viewModel)
}
func showDetail() {
let detailNavigationModel = DetailNavigationModel()
.onMessageReceived { [weak self] message in
switch message {
case _ as FinishedNavigationMessage:
self?.viewModel.dismissalCount += 1
default:
break
}
}
execute(.present(.sheet(.stacked(detailNavigationModel))))
}
}
@MainActor class HomeViewModel: ObservableObject {
@Published var dismissalCount = 0
private unowned let navigationModel: HomeNavigationModel
init(dismissalCount: Int = 0, navigationModel: HomeNavigationModel) {
self.dismissalCount = dismissalCount
self.navigation
