Type-safe, coordinator-driven navigation for SwiftUI apps using only native APIs (NavigationStack, .sheet, .fullScreenCover).
Swift Platform DocC Deploy Documentation
- Overview
- Key Features
- Requirements
- Installation
- Quick Start
- MVVM-C Decoupling
- State Restoration
- Deep Linking
- Testing
- Documentation
SwiftNavigation provides a modular MVVM-C navigation layer with:
- Pure SwiftUI navigation and presentation APIs.
@Observable+@MainActorcoordinator state.- Type-safe stack operations over plain route arrays.
- Nested modal flows with independent internal stacks.
- Codable snapshots for restoration and deep-link reconstruction.
- Type-safe root stack (
[Route]) withpush,pop,popToRoot, andpopToRoute. - Recursive modal navigation (
sheet+fullScreenCover) with internal stack state per modal. - Automatic state sync when users dismiss modals via native gestures.
- Scoped coordinators (
application,feature(name:),tab(name:)) and child lifecycle cleanup. - Protocol-based routing contracts to decouple ViewModels from SwiftUI.
- Swift Testing-friendly architecture for deterministic navigation tests.
- iOS 17+
- Swift 6.1+
- Xcode 16+
In Xcode:
File->Add Packages...- Enter:
https://github.com/Erikote04/SwiftNavigation - Select
Up to Next Major Versionand set it to1.0.0 - Add the
SwiftNavigationproduct to your target
dependencies: [ .package(url: "https://github.com/Erikote04/SwiftNavigation", from: "1.0.0") ]
import SwiftNavigation enum AppRoute: String, NavigationRoute { case home case detail case settings } enum AppModalRoute: String, NavigationRoute { case login case terms }
import SwiftNavigation @MainActor let coordinator = NavigationCoordinator<AppRoute, AppModalRoute>(scope: .application)
import SwiftUI import SwiftNavigation @MainActor struct AppRootView: View { @State private var coordinator = NavigationCoordinator<AppRoute, AppModalRoute>(scope: .application) var body: some View { RoutingView( coordinator: coordinator, root: { HomeView( onOpenDetail: { coordinator.push(.detail) }, onOpenLogin: { coordinator.present(.login, style: .sheet) } ) }, stackDestination: { route in switch route { case .home: HomeView(onOpenDetail: {}, onOpenLogin: {}) case .detail: DetailView() case .settings: SettingsView() } }, modalDestination: { route in switch route { case .login: LoginView() case .terms: TermsView() } } ) .navigationCoordinator(coordinator) } }
Create a feature-focused routing protocol and inject it into your ViewModel.
import SwiftNavigation @MainActor protocol AuthRouting: AnyObject { func showTerms() func finishLogin() } @MainActor final class AuthRouter: AuthRouting { private let router: NavigationRouterProxy<AppRoute, AppModalRoute> init(coordinator: NavigationCoordinator<AppRoute, AppModalRoute>) { self.router = NavigationRouterProxy(coordinator: coordinator) } func showTerms() { router.present(.terms, style: .fullScreen) } func finishLogin() { router.dismissTopModal() router.push(.home) } }
let snapshot = coordinator.exportState() let data = try JSONEncoder().encode(snapshot) let restored = try JSONDecoder().decode( NavigationState<AppRoute, AppModalRoute>.self, from: data ) coordinator.restore(from: restored)
import Foundation import SwiftNavigation struct URLResolver: URLDeepLinkResolving { func navigationState(for url: URL) throws -> NavigationState<AppRoute, AppModalRoute> { if url.host == "settings" { return NavigationState(stack: [.home, .settings]) } return NavigationState(stack: [.home]) } } try coordinator.applyURLDeepLink(URL(string: "myapp://settings")!, resolver: URLResolver())
The coordinator is designed for direct state assertions with Swift Testing.
import Testing @testable import SwiftNavigation @Test @MainActor func pushAndPop() { let coordinator = NavigationCoordinator<AppRoute, AppModalRoute>(scope: .feature(name: "test")) coordinator.push(.home) coordinator.push(.detail) #expect(coordinator.stack == [.home, .detail]) _ = coordinator.pop() #expect(coordinator.stack == [.home]) }
- DocC site: https://erikote04.github.io/SwiftNavigation/documentation/swiftnavigation/
- CI workflow: publish-docc.yml