XCoordinator
π Powerful navigation library for iOS based on the coordinator pattern
Install / Use
/learn @QuickBirdEng/XCoordinatorREADME
β οΈ We have recently released XCoordinator 2.0. Make sure to read this section before migrating. In general, please replace all AnyRouter by either UnownedRouter (in viewControllers, viewModels or references to parent coordinators) or StrongRouter in your AppDelegate or for references to child coordinators. In addition to that, the rootViewController is now injected into the initializer instead of being created in the Coordinator.generateRootViewController method.
βHow does an app transition from one view controller to another?β. This question is common and puzzling regarding iOS development. There are many answers, as every architecture has different implementation variations. Some do it from within the implementation of a view controller, while some use a router/coordinator, an object connecting view models.
To better answer the question, we are building XCoordinator, a navigation framework based on the Coordinator pattern. It's especially useful for implementing MVVM-C, Model-View-ViewModel-Coordinator:
<p align="center"> <img src="https://user-images.githubusercontent.com/15239005/221913797-1ebf0fc8-36d5-4a93-b6da-0a86b6105e6a.png"> </p>πββοΈGetting started
Create an enum with all of the navigation paths for a particular flow, i.e. a group of closely connected scenes. (It is up to you when to create a Route/Coordinator. As our rule of thumb, create a new Route/Coordinator whenever a new root view controller, e.g. a new navigation controller or a tab bar controller, is needed.).
Whereas the Route describes which routes can be triggered in a flow, the Coordinator is responsible for the preparation of transitions based on routes being triggered. We could, therefore, prepare multiple coordinators for the same route, which differ in which transitions are executed for each route.
In the following example, we create the UserListRoute enum to define triggers of a flow of our application. UserListRoute offers routes to open the home screen, display a list of users, to open a specific user and to log out. The UserListCoordinator is implemented to prepare transitions for the triggered routes. When a UserListCoordinator is shown, it triggers the .home route to display a HomeViewController.
enum UserListRoute: Route {
case home
case users
case user(String)
case registerUsersPeek(from: Container)
case logout
}
class UserListCoordinator: NavigationCoordinator<UserListRoute> {
init() {
super.init(initialRoute: .home)
}
override func prepareTransition(for route: UserListRoute) -> NavigationTransition {
switch route {
case .home:
let viewController = HomeViewController.instantiateFromNib()
let viewModel = HomeViewModelImpl(router: unownedRouter)
viewController.bind(to: viewModel)
return .push(viewController)
case .users:
let viewController = UsersViewController.instantiateFromNib()
let viewModel = UsersViewModelImpl(router: unownedRouter)
viewController.bind(to: viewModel)
return .push(viewController, animation: .interactiveFade)
case .user(let username):
let coordinator = UserCoordinator(user: username)
return .present(coordinator, animation: .default)
case .registerUsersPeek(let source):
return registerPeek(for: source, route: .users)
case .logout:
return .dismiss()
}
}
}
Routes are triggered from within Coordinators or ViewModels. In the following, we describe how to trigger routes from within a ViewModel. The router of the current flow is injected into the ViewModel.
class HomeViewModel {
let router: UnownedRouter<HomeRoute>
init(router: UnownedRouter<HomeRoute>) {
self.router = router
}
/* ... */
func usersButtonPressed() {
router.trigger(.users)
}
}
π Organizing an app's structure with XCoordinator
In general, an app's structure is defined by nesting coordinators and view controllers. You can transition (i.e. push, present, pop, dismiss) to a different coordinator whenever your app changes to a different flow. Within a flow, we transition between viewControllers.
Example: In UserListCoordinator.prepareTransition(for:) we change from the UserListRoute to the UserRoute whenever the UserListRoute.user route is triggered. By dismissing a viewController in UserListRoute.logout, we additionally switch back to the previous flow - in this case the HomeRoute.
To achieve this behavior, every Coordinator has its own rootViewController. This would be a UINavigationController in the case of a NavigationCoordinator, a UITabBarController in the case of a TabBarCoordinator, etc. When transitioning to a Coordinator/Router, this rootViewController is used as the destination view controller.
π Using XCoordinator from App Launch
To use coordinators from the launch of the app, make sure to create the app's window programmatically in AppDelegate.swift (Don't forget to remove Main Storyboard file base name from Info.plist). Then, set the coordinator as the root of the window's view hierarchy in the AppDelegate.didFinishLaunching. Make sure to hold a strong reference to your app's initial coordinator or a strongRouter reference.
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
let window: UIWindow! = UIWindow()
let router = AppCoordinator().strongRouter
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
router.setRoot(for: window)
return true
}
}
π€ΈββοΈ Extras
For more advanced use, XCoordinator offers many more customization options. We introduce custom animated transitions and deep linking. Furthermore, extensions for use in reactive programming with RxSwift/Combine and options to split up huge routes are described.
π Custom Transitions
Custom animated transitions define presentation and dismissal animations. You can specify Animation objects in prepareTransition(for:) in your coordinator for several common transitions, such as present, dismiss, push and pop. Specifying no animation (nil) results in not overriding previously set animations. Use Animation.default to reset previously set animation to the default animations UIKit offers.
class UsersCoordinator: NavigationCoordinator<UserRoute> {
/* ... */
override func prepareTransition(for route: UserRoute) -> NavigationTransition {
switch route {
case .user(let name):
let animation = Animation(
presentationAnimation: YourAwesomePresentationTransitionAnimation(),
dismissalAnimation: YourAwesomeDismissalTransitionAnimation()
)
let viewController = UserViewController.instantiateFromNib()
let viewModel = UserViewModelImpl(name: name, router: unownedRouter)
viewController.bind(to: viewModel)
return .push(viewController, animation: animation)
/* ... */
}
}
}
π€ Deep Linking
Deep Linking can be used to chain different routes together. In contrast to the .multiple transition, deep linking can identify routers based on previous transitions (e.g. when pushing or presenting a router), which enables chaining of routes of different types. Keep in mind, that you cannot access higher-level routers anymore once you trigger a route on a lower level of the router hierarchy.
class AppCoordinator: NavigationCoordinator<AppRoute> {
/* ... */
override func prepareTransition(for route: AppRoute) -> NavigationTransition {
switch route {
/* ... */
case .deep:
return deepLink(AppRoute.login, AppRoute.home, HomeRoute.news, HomeRoute.dismiss)
}
}
}
β οΈ XCoordinator does not check at compile-time, whether a deep link can be executed. Rather it uses assertionFailures to inform about incorrect chaining at runtime, when it cannot find an appropriate router for a given route. Keep this in mind when changing the structure of your app.
π RedirectionRouter
Let's assume, there is a route type called HugeRoute with more than 10 routes. To decrease coupling, HugeRoute needs to be split up into multiple route types. As you will discover, many routes in HugeRoute use transitions dependent on a specific rootViewController, such as push, show, pop, etc. If splitting up routes by introducing a new router/coordinator is not an option, XCoordinator has two solutions for you to solve such a case: RedirectionRouter or using multiple coordinators with the same rootViewController (see this section for more information).
A RedirectionRouter can b
