This content originally appeared on HackerNoon and was authored by Ilia Kuznetsov
\ In this article, I’ll introduce a Coordinators framework for designing navigation in your SwiftUI app using the coordinator pattern. This pattern abstracts navigation logic away from individual views, centralizing it within "coordinator" objects.
\
SwiftUI provides built-in navigation tools like NavigationStack
and NavigationLink
, but as applications become more complex, managing navigation through a centralized coordinator simplifies dependencies and state management. Coordinators handle navigation, dependency injection, and deep linking, making SwiftUI views lightweight and focused on UI concerns.
\ Complex applications may have multiple coordinators, each responsible for a specific feature or flow, such as user authorization, configuration wizard, settings, etc.
Key Features
- Views Focused on UI: Views handle their own UI and state but do not manage navigation.
\
- No Screen-to-Screen Knowledge: Each screen doesn’t know how to construct another screen. The coordinator is solely responsible for creating and displaying new screens, which keeps views reusable and decoupled.
\
- Dependency Injection: Coordinators inject dependencies into views, making data flow and service access streamlined and helping avoid singletons.
\
- State Management Across Screens: Coordinators can manage the state across multiple screens, ensuring logical progression across a navigation flow.
\
- Deep Linking: Coordinators simplify deep linking by determining the correct sequence of screens to present, providing a seamless user experience.
Adding to a Project
The framework is distributed via Swift Package Manager (SPM). To integrate it, add a link to the package in your project’s package dependencies tab.
\
\
Navigation Types
This framework was designed to keep your code minimal, and basically, it follows the familiar logic we had in UIKit projects.
\ You may notice the similarity between UINavigationController and UIKit presentation logic.
\ In the iOS application, we have three main types of navigation:
- Navigation Stack
- Modal Sheet
- Tabs
\ Let’s look at the implementation of each of them.
Navigation Stack
NavigationStack allows users to navigate through multiple layers of views, maintaining a stack-like structure for managing the view hierarchy. Each screen “pushed” onto the stack represents a deeper level in the navigation sequence. Usually, the navigation of the new screen is accompanied by horizontal animation. Users can go back by “popping” views off the stack using a back button or swiping gesture.
\
\ Start by creating screens you’ll navigate between.
import SwiftUI
struct FirstScreen: View {
var body: some View {
Color.red
}
}
struct SecondScreen: View {
var body: some View {
Color.blue
}
}
struct ThirdScreen: View {
var body: some View {
Color.yellow
}
}
\ Now, we can create a simple navigation coordinator.
import SwiftUI
import Coordinators
class CommonCoordinator: NavigationCoordinator {
// screens available for navigation
enum Screen: ScreenProtocol {
case first
case second
case third
}
// view for each screen
func destination(for screen: Screen) -> some View {
switch screen {
case .first: FirstScreen()
case .second: SecondScreen()
case .third: ThirdScreen()
}
}
}
\ The NavigationCoordinator protocol requires you to implement an enumeration of screens and a function to construct views for each screen. That’s it.
\ Now, initialize the coordinator at the root level of the app.
@main
struct CoordinatorsExampleApp: App {
// create an instance of the coordinator
@StateObject var coordinator = CommonCoordinator()
var body: some Scene {
WindowGroup {
// present root view of coordinator
coordinator.view(for: .first)
}
}
}
\ In the current example, the coordinator is stored as a StateObject, and its initial screen (.first) is presented as the app’s root view.
\ To navigate between screens, modify the first screen to include buttons for navigation.
struct FirstScreen: View {
// reference to the coordinator
@EnvironmentObject var coordinator: Navigation<CommonCoordinator>
var body: some View {
VStack {
Button("Second") {
// navigation to the second screen
coordinator().present(.second)
}
Button("Third") {
// navigation to the third screen
coordinator().present(.third)
}
}
}
}
\
Our coordinator is passed to all the children's views as @EnvironmentObject
in the Navigation
wrapper. Now, you can navigate to the other screens using the function coordinator().present()
. This function accepts only screens that this coordinator can present.
\ To go back, you can use well-known dismiss environment value:
@Environment(\.dismiss) var dismiss
\ Or if you need additional options you can use a coordinator reference:
coordinator().pop()
coordinator().popTo(.first)
coordinator().popToRoot()
coordinator().popTo(where: { screen in })
\
Modal Sheet
In modal navigation, a new screen is presented over the current one, covering it partially or entirely. This approach temporarily interrupts the main navigation flow, allowing users to focus on a new task or piece of content, with the expectation that they’ll eventually return to the previous screen.
\
\
To support modal navigation, make your coordinator conform to ModalCoordinator
. It follows similar logic to NavigationCoordinator
, but we also can present child coordinators.
class CommonCoordinator: NavigationCoordinator, ModalCoordinator {
//--
// screens or navigation stacks available to be presented modally
enum Modal: ModalProtocol {
case firstModal
case secondModal
case child(ChildNavigationCoordinator = .init())
}
// view for each modal screen
func destination(for modal: Modal) -> some View {
switch modal {
case .firstModal: FirstScreen()
case .secondModal: SecondScreen()
case .child(let coordiantor): coordiantor.view(for: .first)
}
}
}
\ The modal presentation style can also be customized.
enum Modal: ModalProtocol {
case first
case second
case child(ChildNavigationCoordinator = .init())
var style: ModalStyle {
switch self {
case .first: .cover
case .second: .overlay
case .child(let childNavigationCoordinator): .sheet
}
}
}
\
To present modal flow, you will use present()
function:
Button("Present Modally") {
coordinator().present(.child())
}
\ To dismiss it the same as before:
@Environment(\.dismiss) var dismiss
\ Or referencing the coordinator
coordinator().dismiss()
coordinator().dismissPresented()
The first one is for dismissing the current coordinator, and the second one is to dismiss the modal screen presented over the current coordinator.
Tab Navigation and Others
Tab-based navigation allows users to switch between different sections of the app by tapping on icons typically located at the bottom of the screen. Each tab represents a distinct area of the app, allowing for easy and quick access to different parts of the app without disrupting the user’s current context.
\
\
For implementing such navigation or any other similar one, you can use protocol CustomCoordinator
.
\ You need to implement your own view and pass it to a destination function.
class TabsCoordinator: CustomCoordinator {
enum Tabs: Hashable {
case tab1
case tab2
case tab3
}
@Published var currentTab: Tabs = .tab1
let tab1 = CommonCoordinator()
let tab2 = CommonCoordinator()
let tab3 = CommonCoordinator()
func destination() -> some View {
TabsScreen(coordinator: self)
}
struct TabsScreen: View {
@ObservedObject var coordinator: TabsCoordinator
var body: some View {
TabView(selection: $coordinator.currentTab) {
coordinator.tab1.view(for: .first)
.tabItem { Label("First", systemImage: "circle") }
.tag(Tabs.tab1)
coordinator.tab2.view(for: .first)
.tabItem { Label("Second", systemImage: "circle") }
.tag(Tabs.tab2)
coordinator.tab3.view(for: .first)
.tabItem { Label("Third", systemImage: "circle") }
.tag(Tabs.tab3)
}
}
}
}
\
And use a rootView
property in the view hierarchy.
@main
struct CoordinatorsExampleApp: App {
@StateObject var coordinator = TabsCoordinator()
var body: some Scene {
WindowGroup {
coordinator.rootView
}
}
}
\
Dependencies Management
Coordinators simplify dependency injection, passing services to views directly and enabling easier testing with mock services. You don’t need to use singletons or pass services to views deep down through the view hierarchy.
class CommonCoordinator: NavigationCoordinator, ModalCoordinator {
let someService: SomeService
let anotherService: SomeService
enum Screen: ScreenProtocol {
case first
case second
case third
}
func destination(for screen: Screen) -> some View {
switch screen {
case .first: FirstScreen(someService: self.someService)
case .second: SecondScreen(anotherService: self.anotherService)
case .third: ThirdScreen()
}
}
}
\
Deep Linking
It becomes easy to handle deep links, just need to add a URL handler to your root coordinator.
@main
struct CoordinatorsExampleApp: App {
@StateObject var coordinator = CommonCoordinator()
var body: some Scene {
WindowGroup {
coordinator.view(for: .first).onOpenURL { url in
coordinator.handle(url: url)
}
}
}
}
\ And present a corresponding screen or even navigation flow.
class CommonCoordinator: NavigationCoordinator, ModalCoordinator {
//--
@MainActor
func handle(url: URL) {
let showModal: Bool
// parse an url
if showModal {
// create a child coordinator presenting some screen if needed
let childCoordinator = ChildNavigationCoordinator()
childCoordinator.present(.deepLinkScreen)
// present child coordinator modally
present(.child(childCoordinator), resolve: .replaceCurrent)
}
}
}
\
Conclusion
This article has covered the basics of using the Coordinators framework, but it also offers even more advanced capabilities by accessing the coordinator’s navigation state. You can observe and modify it enabling highly customized and complex workflows.
\ Using the Coordinators framework centralizes navigation and dependency management, leading to cleaner, modular code that’s easier to test, maintain, and scale. By abstracting navigation logic, coordinators keep views lightweight, reusable, and focused purely on UI concerns.
\ With this framework, your SwiftUI apps will be better structured, more maintainable, and ready to handle complex navigation flows—whether through deep linking, dependency injection, or multi-layered navigation stacks.
\ ==Link== to the Coordinators framework on GitHub.
\ Please, put a star on it if you like it.
\ Happy Coding!
This content originally appeared on HackerNoon and was authored by Ilia Kuznetsov
Ilia Kuznetsov | Sciencx (2024-11-07T01:41:18+00:00) Proper Navigation in SwiftUI With Coordinators: A Guide. Retrieved from https://www.scien.cx/2024/11/07/proper-navigation-in-swiftui-with-coordinators-a-guide/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.