SwiftUICoordinator is a powerful implementation of the Coordinator pattern specifically designed for SwiftUI applications. It provides a robust solution for managing navigation flows while maintaining clean architecture principles and separation of concerns.
- ποΈ Modular Architecture: Clear separation between navigation logic and view presentation
- π Flexible Navigation: Support for stack-based, modal, and tab bar navigation
- π Deep Linking: Built-in support for handling deep links
- π¨ Custom Transitions: Extensible transition system
- π± iOS 15+ Support
iOS 15.0+
dependencies: [
.package(url: "https://github.com/erikdrobne/SwiftUICoordinator")
]

The foundation of navigation flow management:
@MainActor
protocol Coordinator: AnyObject {
var parent: Coordinator? { get }
var childCoordinators: [Coordinator] { get set }
var name: String { get }
func handle(_ action: CoordinatorAction)
func add(child: Coordinator)
func remove(coordinator: Coordinator)
}
Manages the navigation stack and view presentation:
@MainActor
public protocol Navigator: ObservableObject {
associatedtype Route: StackNavigationRoute
var navigationController: UINavigationController { get }
var startRoute: Route { get }
func start()
func show(route: Route)
func set(routes: [Route], animated: Bool)
func append(routes: [Route], animated: Bool)
func pop(animated: Bool)
func popToRoot(animated: Bool)
func dismiss(animated: Bool)
}
// Combine Navigator and Coordinator
public typealias Routing = Coordinator & Navigator
Define your navigation paths:
protocol NavigationRoute {
var title: String? { get }
var appearance: RouteAppearance? { get }
var hidesNavigationBar: Bool? { get }
}
protocol StackNavigationRoute: NavigationRoute {
var action: TransitionAction { get }
var hidesBackButton: Bool? { get }
}
// Example Implementation
enum AuthRoute: StackNavigationRoute {
case login
case signup
case resetPassword
var action: TransitionAction {
return .push(animated: true)
}
}
Defines the available actions for the coordinator. Views should exclusively interact with the coordinator through actions, ensuring a unidirectional flow of communication.
protocol CoordinatorAction {
var name: String { get }
}
// Example Implementation
enum AuthAction: CoordinatorAction {
case didLogin
case didSignup
case showSignup
case showLogin
case showResetPassword
}
Connect routes to views:
@MainActor
protocol RouterViewFactory {
associatedtype V: View
associatedtype Route: NavigationRoute
@ViewBuilder
func view(for route: Route) -> V
}
import SwiftUICoordinator
Start by creating an enum with all the available routes for a particular coordinator flow.
enum AuthRoute: StackNavigationRoute {
case login
case signup
case resetPassword
var action: TransitionAction {
return .push(animated: true)
}
}
Specify custom actions that can be sent from coordinated objects to their coordinators.
enum AuthAction: CoordinatorAction {
case didLogin
case didSignup
case showLogin
case showSignup
case showResetPassword
}
The coordinator has to conform to the Routing
protocol.
final class AuthCoordinator: Routing {
weak var parent: Coordinator?
var childCoordinators = [Coordinator]()
let navigationController: UINavigationController
let startRoute: AuthRoute
init(
parent: Coordinator?,
navigationController: NavigationController,
startRoute: AuthRoute = .login
) {
self.parent = parent
self.navigationController = navigationController
self.startRoute = startRoute
}
func handle(_ action: CoordinatorAction) {
switch action {
case AuthAction.didLogin:
parent?.handle(Action.done(self))
case AuthAction.showSignup:
show(route: .signup)
case AuthAction.showLogin:
pop()
default:
parent?.handle(action)
}
}
}
// Connect views to routes
extension AuthCoordinator: RouterViewFactory {
@ViewBuilder
func view(for route: AuthRoute) -> some View {
switch route {
case .login:
LoginView(viewModel: LoginViewModel(coordinator: self))
case .signup:
SignupView(viewModel: SignupViewModel(coordinator: self))
case .resetPassword:
ResetPasswordView(viewModel: ResetPasswordViewModel(coordinator: self))
}
}
}
SwiftUICoordinator also supports creating custom transitions.
final class FadeTransition: NSObject, Transitionable {
func isEligible(
from fromRoute: NavigationRoute,
to toRoute: NavigationRoute,
operation: NavigationOperation
) -> Bool {
// Define when this transition should be used
return true
}
func animateTransition(using context: UIViewControllerContextTransitioning) {
guard let toView = context.view(forKey: .to) else {
context.completeTransition(false)
return
}
let containerView = context.containerView
toView.alpha = 0.0
containerView.addSubview(toView)
UIView.animate(
withDuration: transitionDuration(using: context),
animations: {
toView.alpha = 1.0
},
completion: { _ in
context.completeTransition(!context.transitionWasCancelled)
}
)
}
}
// Register transitions
let factory = NavigationControllerFactory()
let transitions = [FadeTransition()]
lazy var delegate = factory.makeTransitionDelegate(transitions)
lazy var navigationController = factory.makeNavigationController(delegate: self.delegate)
First, define a transition delegate object that conforms to the UIViewControllerTransitioningDelegate
protocol.
final class SlideTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return SlideTransition(isPresenting: true)
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return SlideTransition(isPresenting: false)
}
}
In this example, SlideTransition
is a custom class that conforms to the UIViewControllerAnimatedTransitioning
protocol and handles the actual animation logic.
Pass the SlideTransitionDelegate
instance to the specific action where you wish to apply your modal transition.
var action: TransitionAction? {
switch self {
case .rect:
return .present(delegate: SlideTransitionDelegate())
default:
return .push(animated: true)
}
}
In your application, you can handle deep links by creating a DeepLinkHandler
that conforms to the DeepLinkHandling
protocol. This handler will specify the URL scheme and the supported deep links that your app can recognize.
class DeepLinkHandler: DeepLinkHandling {
static let shared = DeepLinkHandler()
let scheme = "coordinatorexample"
let links: Set<DeepLink> = [
DeepLink(action: "cart", route: CatalogRoute.cart)
]
private init() {}
}
To handle incoming deep links in your app, you can implement the scene(_:openURLContexts:)
method in your scene delegate.
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard
let url = URLContexts.first?.url,
let deepLink = try? dependencyContainer.deepLinkHandler.link(for: url),
let params = try? dependencyContainer.deepLinkHandler.params(for: url, and: deepLink.params)
else {
return
}
dependencyContainer.appCoordinator?.handle(deepLink, with: params)
}
For better understanding, I recommend that you check the example project located in the SwiftUICoordinatorExample
directory.
Contributions are welcome to help improve and grow this project!
If you come across a bug, kindly open an issue on GitHub, providing a detailed description of the problem. Include the following information:
- steps to reproduce the bug
- expected behavior
- actual behavior
- environment details
For feature requests, please open an issue on GitHub. Clearly describe the new functionality you'd like to see and provide any relevant details or use cases.
To submit a pull request:
- Fork the repository.
- Create a new branch for your changes.
- Make your changes and test thoroughly.
- Open a pull request, clearly describing the changes you've made.
Thank you for contributing to SwiftUICoordinator! π
If you appreciate this project, kindly give it a βοΈ to help others discover the repository.