A modern approach to handling navigation in SwiftUI using an imperative style. This method simplifies view logic by extracting navigation concerns, improving testability, and making the codebase more maintainable.
Medium Article: Link
While SwiftUI promotes declarative navigation with NavigationStack and NavigationLink, complex navigation flows can become difficult to manage. Imperative navigation allows:
- Decoupling navigation logic from views.
 - Better testability by moving navigation handling to a dedicated coordinator.
 - Improved maintainability by centralizing navigation in a single source of truth.
 
- Navigation managed outside of views.
 - Supports deep linking and complex navigation flows.
 - Clean and structured architecture using a navigation coordinator.
 
- iOS 16.0+
 - Xcode 15+
 - Swift 5.9+
 
Clone the repository:
git clone https://github.com/ahmdmhasn/swiftui-imperative-navigation.git
cd swiftui-imperative-navigationOpen SwiftUIImperativeNavigation.xcodeproj in Xcode and run the sample project.
The NavigationCoordinator acts as the central controller for handling navigation events.
@MainActor
public final class NavigationController: ObservableObject {
    func push<V: View>(_ view: V)
    func pop()
    func popToRoot()
    func present<V: View>(_ view: V)
    func sheet<V: View>(_ view: V)
    func dismiss()
}The NavigationView works with the NavigationController.
struct NavigationView<Root: View>: View {
    ...
    var body: some View {
        NavigationStack(path: $controller.path) {
            root
                .navigationDestination(for: Route.self, destination: \.body)
                .fullScreenCover(item: $controller.modal, content: \.route.body)
                .sheet(item: $controller.modal, content: \.route.body)
        }
    }
}@MainActor
final class DefaultCoordinator {
    let navigationController = NavigationController()
    func navigateToB() {
        navigationController.push(ViewB(coordinator: self)) // Push SwiftUI.View
    }
    func navigateToC() {
        let coordinatorC = DefaultCoordinatorC { [weak self] in
            self?.navigationController.dismiss() // Dismiss presented modal
        }
        coordinatorC.start(from: navigationController) // Present coordinator
    }
}struct HomeView: View {
    let coordinator: Coordinator
    
    var body: some View {
        Button("Go to Details") {
            coordinator.navigateToB()
        }
    }
}- Add unit tests for navigation logic.
 - Extend examples for more navigation patterns (modals, tab-based navigation).
 - Provide better state persistence handling.
 
Contributions are welcome! Feel free to open an issue or submit a pull request.
This project is licensed under the MIT License. See the LICENSE file for details.
If you find this project helpful, give it a ⭐ on GitHub!
