SkillAgentSearch skills...

SwiftUINavigation

Framework for Implementing Clean Navigation in SwiftUI

Install / Use

/learn @RobertDresler/SwiftUINavigation

README

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 fullScreenCover transitions (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 present SFSafariViewController or UIActivityViewController
  • ✅ 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 Shared module 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 the Dependencies folder.
  • The ActionableList module 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’s Data/Factories/... folder.

Installation

  1. 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)
  2. Open the app at path SwiftUINavigation/Examples.xcodeproj

  3. Run the app

    • On simulator
    • On a real device (set your development team)
  4. (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.

  5. Explore the app

</details>

Explore on Your Own

<details> <summary>Click to see details 👈</summary>
  1. To get started, first add the package to your project:
  • In Xcode, add the package by using this URL: https://github.com/RobertDresler/SwiftUINavigation and choose the dependency rule up to next major version from 2.2.2
  • Alternatively, add it to your Package.swift file: .package(url: "https://github.com/RobertDresler/SwiftUINavigation", from: "2.2.2")
  1. (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
View on GitHub
GitHub Stars114
CategoryDevelopment
Updated16d ago
Forks10

Languages

Swift

Security Score

100/100

Audited on Mar 17, 2026

No findings