This project provides a lightweight Navigation Coordinator, using SwiftUI NavigationStack (available from iOS 16).
The current implementation covers 6 main transitions:
Stack Navigation:
push
— navigates forward to a new view.pop
— returns to the previous view.unwind
— performs a multi-level return.popToRoot
— returns to the root view.
Modal Presentation:
present
— displays a modal view, overlaying it on top of current content.dismiss
— closes the current modal view and returns to the underlying content.
- iOS: iOS 16.0+
- macOS: macOS 13.0+
- watchOS: watchOS 9.0+
- tvOS: tvOS 16.0+
- Open Xcode and select “File” > “Add Packages…”
- Enter the URL of the package repository.
- Follow the instructions to complete the installation.
1. Configure the NavigableScreen Enum
Start by creating an enum Screen to represent the different screens in your app. Ensure it conforms to the NavigableScreen protocol:
import NavigationCoordinator
enum Screen {
case login
case movies
case settings
// etc.
}
extension Screen: NavigableScreen {
@ViewBuilder
var view: some View {
switch self {
case .login: LoginView()
case .movies: MoviesView()
case .settings: SettingsView()
}
}
}
2. Define Typealiases
Define typealias to simplify the usage of the types used with your coordinator:
import NavigationCoordinator
typealias SegueModifier = RegisterSegueModifier<Screen>
typealias Coordinator = NavigationCoordinator<Screen>
typealias RootView = NavigationStackRootView<Screen>
3. Configure the App Entry Point
Set up the app entry point using the RootView to define the initial screen:
import SwiftUI
@main
struct MainApp: App {
var body: some Scene {
WindowGroup {
RootView(.login)
}
}
}
Push
import SwiftUI
struct LoginView: View {
@EnvironmentObject var coordinator: Coordinator
var body: some View {
Button("Movies") {
coordinator.push(.movies)
}
}
}
Pop
import SwiftUI
struct MoviesView: View {
@EnvironmentObject var coordinator: Coordinator
var body: some View {
Button("back") {
coordinator.pop()
}
}
}
PopToRoot
import SwiftUI
struct SettingsView: View {
@EnvironmentObject var coordinator: Coordinator
var body: some View {
Button("login") {
coordinator.popToRoot()
}
}
}
Unwind
Use a unique identifier for your unwind segues. If a segue becomes no longer relevant, it will be automatically removed from the coordinator. Using `onUnwind()` modifier is completely safe, tested, and does not involve any memory leaks or unintended calls.import SwiftUI
// B View
// 🟦🟦🅰🟦🟦🟦🟦🟦🟦🅱️
struct B: View {
@EnvironmentObject var coordinator: Coordinator
var body: some View {
Button("pop to A") {
coordinator.unwind(to: "identifier" /*, with: Any?*/)
}
}
}
// A View
// 🟦🟦🅰️
struct A: View {
var body: some View {
VStack {}
.onUnwind(segue: "identifier") /*{ Any? in }*/
}
}
onUnwind()
will always be called before onAppear()
.
Present
import SwiftUI
/*
[B]
[ ][ ][ ][ ][ ][A]
*/
struct A: View {
@EnvironmentObject var coordinator: Coordinator
var body: some View {
Button("present") {
coordinator.present(.B)
}
}
}
Dismiss
import SwiftUI
/*
[B][ ][ ][ ][CL]
[ ][ ][ ][ ][ ][A]
*/
struct CL: View {
@EnvironmentObject var coordinator: Coordinator
var body: some View {
Button("dismiss") {
coordinator.dismiss(/*to: "identifier" /*, with: Any?*/*/)
}
}
}
/*
[ ][ ][ ][ ][ ][A]
*/
struct A: View {
@EnvironmentObject var coordinator: Coordinator
var body: some View {
VStack {}
// Not necessary. Only if you need to capture an onDismiss event.
.onDismiss(segue: "identifier") /*{ Any? in }*/
}
}
I welcome any issues you find within the project. If you encounter bugs or have suggestions for improvements, please feel free to create an issue on the GitHub repository.
Apache License 2.0. See the LICENSE file for details.