# XCoordinator: a coordinator-pattern navigation library for iOS

> XCoordinator routes iOS screen transitions through typed route enums and coordinator objects instead of view controllers. It fits MVVM-C projects that want navigation out of view models, and it assumes you are comfortable with Swift generics and a programmatic window setup.

**QuickBirdEng/XCoordinator** — 🎌 Powerful navigation library for iOS based on the coordinator pattern

- Repository: https://github.com/QuickBirdEng/XCoordinator
- Stars: 2,392 · Forks: 190
- Language: Swift
- License: MIT
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/quickbirdeng-xcoordinator

## The navigation question XCoordinator answers

The README opens with the question it was built around: how does an app transition from one view controller to another. Its answer is to move that decision out of the view controller and into a coordinator, an object that connects view models and owns transitions. The stated target is MVVM-C, Model-View-ViewModel-Coordinator, where the view model holds no reference to a concrete screen and instead asks a router to trigger a route. This matters most in apps where the same screen is reachable from several places, because the destination logic lives in one coordinator rather than being duplicated at each call site. The README's rule of thumb is to create a new Route and Coordinator whenever a new root view controller is needed, such as a new navigation controller or tab bar controller. That is a structural decision, not a stylistic one: it means the coordinator tree mirrors the container hierarchy of the app.

## Route enums, coordinators and the transition switch

The mechanism has two parts. A Route is an enum listing every trigger in a flow; a Coordinator subclasses a container-specific base such as NavigationCoordinator or TabBarCoordinator and implements prepareTransition(for:), returning a transition for each case. In the README example, UserListRoute covers home, users, user(String), registerUsersPeek(from:) and logout, and each case returns a concrete transition such as .push, .present or .dismiss. Because a coordinator maps routes to transitions, the README notes that multiple coordinators can be prepared for the same route and differ in which transitions they execute. Routes are triggered from coordinators or view models; the README shows a HomeViewModel holding a UnownedRouter<HomeRoute> and calling router.trigger(.users).

The nesting rule is the part worth internalising. Every coordinator has its own rootViewController, a UINavigationController for a NavigationCoordinator, a UITabBarController for a TabBarCoordinator. When you transition to another coordinator, that root view controller becomes the destination. So a flow change means presenting or pushing a coordinator, while movement inside a flow means pushing view controllers. The README's UserListCoordinator example does exactly that: .user(String) builds a UserCoordinator and returns .present(coordinator, animation: .default), and .logout returns .dismiss() to fall back to the previous flow.

## Installing XCoordinator and wiring the first route

The repository ships a podspec, a Package.swift and Carthage support, so the dependency itself is not the hard part. The harder part is launch. The README instructs you to create the app window programmatically in AppDelegate.swift and to remove Main Storyboard file base name from Info.plist. If you skip that step, the coordinator never becomes the root of the window hierarchy.

Start by declaring the routes for your first flow as an enum conforming to Route:

```swift
enum UserListRoute: Route {
    case home
    case users
    case user(String)
    case logout
}
```

Then subclass a container coordinator, pass an initial route to super, and return a transition per case. The README's UserListCoordinator starts with super.init(initialRoute: .home) and returns .push(viewController) for .home. In production you would build the view controller and its view model here, injecting unownedRouter into the view model so it can trigger further routes.

Finally, set the coordinator as the root of the window in didFinishLaunching. The README's AppDelegate holds the router as a strongRouter property and calls router.setRoot(for: window):

```swift
@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
    }
}
```

If the screen stays black, the usual cause is a missing strong reference to the coordinator or a leftover storyboard entry in Info.plist. The README states both requirements explicitly.

## The 2.0 migration is the real adoption cost

The README carries a warning about the 2.0 release, and it is the most consequential thing on the page for anyone upgrading. AnyRouter was split into UnownedRouter and StrongRouter. UnownedRouter belongs in view controllers, view models and references to parent coordinators; StrongRouter belongs in the AppDelegate or in references to child coordinators. That distinction is not cosmetic. A router that a view model holds must not keep the coordinator alive, while a child coordinator held by its parent must be retained, and getting the choice wrong produces either a leak or a coordinator that deallocates while a screen still needs it.

The second breaking change is that the rootViewController is now injected into the initializer instead of being created inside Coordinator.generateRootViewController. Any code that overrode that method no longer applies. The README does not document a rollback path, and the release history shows 2.2.1 in February 2023 after 2.2.0 and 2.1.0 in March 2022, so the 2.x line has been stable for a while rather than churning. Treat the migration as a mechanical but wide-reaching edit: every AnyRouter reference needs a decision, and every root view controller construction needs to move to the call site.

## Where XCoordinator is the wrong tool

Two constraints stand out. First, the library is iOS-only. The README badges list iOS as the platform, and the container coordinators are built around UIKit types such as UINavigationController and UITabBarController. A SwiftUI-first app, or one targeting macOS, has no path here.

Second, the launch model assumes you own the window. The README tells you to create the window programmatically and remove the main storyboard entry from Info.plist. Teams with a large existing storyboard-driven app face a migration before they can use a single route. There is also a conceptual cost that the README does not discuss: once view models hold a router, navigation becomes a testable dependency, but it also means every view model carries a generic parameter and a trigger call, which some teams find heavier than a delegate or a closure passed at construction. The README does not document rollback or a mixed mode where only part of the app uses coordinators, so partial adoption is not described.

## XCoordinator compared with URLNavigator

URLNavigator, which appears alongside XCoordinator in search results for this library, takes a different route to the same problem. It maps URL patterns to view controllers, so navigation is expressed as a string or URL and resolved by a registry at runtime. XCoordinator expresses navigation as Swift enum cases resolved by a coordinator at compile time. The practical difference is where mistakes surface. A mistyped URL pattern in URLNavigator fails at runtime when nothing matches; a route case missing from the switch in prepareTransition(for:) fails at compile time because the switch is exhaustive over the enum. In exchange, XCoordinator couples navigation to Swift types and to the coordinator hierarchy, while URL-based routing can be driven from outside the app, for example from a deep link. The README mentions deep linking under its extras section, so the two approaches overlap there, but the underlying model is not the same.

## Maintenance, licence and what to check before adopting

The repository is not archived, and the last push was on 2026-07-02. The most recent tagged release is 2.2.1 from 2023-02-28, so the gap between pushes and releases is wide: activity on the default branch does not automatically translate into versioned releases, and pinning to a tag gives you code that has not changed in over three years. The project is MIT licensed, which permits commercial use and modification; the repository includes a LICENSE file at the top level. That is a statement about the licence text, not legal advice, and any distributed derivative still needs to carry the licence notice.

Upgrade cost is dominated by the 2.0 router split rather than by routine version bumps. Before adopting, check three things against your own project: whether AppDelegate already builds the window programmatically, whether Info.plist still names a main storyboard, and whether you can hold a strongRouter reference for the lifetime of the app. The README also points to generated documentation at quickbirdstudios.github.io/XCoordinator and a .jazzy.yaml in the repository, so the API reference exists outside the README. The README itself does not cover rollback, partial adoption or SwiftUI.

## Conclusion

Adopt XCoordinator if you are building an iOS app in Swift with MVVM-C and want route enums to own every push, present, pop and dismiss. Do not adopt it if you want storyboard-driven launch or cannot accept that view models receive a router object. Before committing, verify that your AppDelegate creates the window programmatically, that Main Storyboard file base name is removed from Info.plist, and that you keep a strong reference to the initial coordinator or its strongRouter.

## FAQ

### What is an example of a coordinator in XCoordinator?

The README's example declares a UserListRoute enum with cases such as home, users, user(String) and logout, then implements UserListCoordinator as a NavigationCoordinator that returns transitions like .push, .present and .dismiss from prepareTransition(for:).

### What is the role of a coordinator in XCoordinator?

A coordinator prepares the transitions that run for each triggered route, and the README notes that multiple coordinators can be prepared for the same route and differ in which transitions they execute. Each coordinator also owns a rootViewController, such as a UINavigationController or a UITabBarController.

### How do I install XCoordinator?

The repository includes XCoordinator.podspec, Package.swift and Carthage support, so it can be added through those dependency managers. After that, the README requires a programmatic window in AppDelegate and removal of the main storyboard entry from Info.plist.

### What changed in XCoordinator 2.0?

AnyRouter was replaced by UnownedRouter for view controllers, view models and parent coordinator references, and by StrongRouter for the AppDelegate and child coordinator references. The rootViewController is also injected into the initializer instead of being created in Coordinator.generateRootViewController.

### Can I use XCoordinator with SwiftUI or on macOS?

The README lists iOS as the platform and its container coordinators are built on UIKit types such as UINavigationController and UITabBarController. Nothing in the README describes SwiftUI or macOS support.

## Sources

- [Issues](https://github.com/QuickBirdEng/XCoordinator/issues)
- [License: MIT](https://github.com/QuickBirdEng/XCoordinator/blob/master/LICENSE)
- [QuickBirdEng/XCoordinator on GitHub](https://github.com/QuickBirdEng/XCoordinator)
- [README](https://github.com/QuickBirdEng/XCoordinator/blob/master/README.md)
- [Releases](https://github.com/QuickBirdEng/XCoordinator/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/quickbirdeng-xcoordinator
