erikdrobne/swiftuicoordinator
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.
Introduction
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.
Features
- ποΈ 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
Installation
Requirements
iOS 15.0+
Swift Package Manager
dependencies: [
.package(url: "https://github.com/erikdrobne/SwiftUICoordinator")
]π Core Components
<img width="912" alt="workflow" src="https://github.com/erikdrobne/SwiftUICoordinator/assets/15943419/9c9d279c-e87d-43c2-85df-7f197bed01d3">
Coordinator Protocol
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)
}Navigator Protocol
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 & NavigatorNavigation Routes
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)
}
}CoordinatorAction
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
}RouterViewFactory
Connect routes to views:
@MainActor
protocol RouterViewFactory {
associatedtype V: View
associatedtype Route: NavigationRoute
@ViewBuilder
func view(for route: Route) -> V
}π§ Usage
import SwiftUICoordinatorCreate Route
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)
}
}Create Action
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
}Create Coordinator
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))
}
}
}Custom transitions
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)Modal transitions
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)
}
}Handling deep links
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)
}Example project
For better understanding, I recommend that you check the example project located in the SwiftUICoordinatorExample directory.
π€ Contributions
Contributions are welcome to help improve and grow this project!
Reporting bugs
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
Requesting features
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.
Submitting pull requests
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.
Package Metadata
Repository: erikdrobne/swiftuicoordinator
Default branch: main
README: README.md