Library / SDK
hmlongco/Navigator avatar
hmlongco/Navigator

Navigator removes the destination registration ritual from SwiftUI navigation

Advanced Navigation Support for SwiftUI.

542 stars39 forksSwiftMIT

At a glance

What is it?
Navigator is a SwiftUI navigation layer built on NavigationStack that lets a destination type declare its own view, so no navigationDestination registration is needed, and it adds checkpoints, cross-module routing, deep linking and navigation state restoration. Version 2.1.4 is the current release and it requires iOS 17 or later.
Who is it for?
Adopt Navigator if your SwiftUI app has grown past a single NavigationStack and you are tired of registering destination types, especially if you need deep links or a way back to a specific screen. Skip it if your app has a handful of screens, since the plain NavigationStack already handles that.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 56 days ago.
What is it written in?
Mainly Swift, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What Navigator claims to add over a plain NavigationStack

Navigator describes itself as advanced navigation support for SwiftUI, and it opens by saying this is not just another push/pop navigation stack library. The bullet list is the feature set. Simple linking and presentation of views. Coordination patterns with a well-defined separation of concerns. Extensive support for modular applications, including cross-module navigation and views. True deep linking and internal application navigation via navigation send. Returning to a specific spot in the navigation tree via navigation checkpoints, and returning callback values through those checkpoints. Both declarative and imperative navigation and control. Navigation state restoration. Event logging and debugging. And then the last bullet, which the README draws attention to explicitly: no navigationDestination registration operations are required, followed by the line You might want to reread that last bullet point again. The current version, 2.1.4, is written entirely in Swift and SwiftUI, supports Observation, and runs on iOS 17 and above. The licence is MIT and the last push was on 2026-08-28, with releases 2.1.3 on 2026-07-27 and 2.1.2 on 2026-06-29 before it.

Destinations that declare their own view

The core idea is that a destination is an enum that carries its view. Destinations, or routes, are typically just public lists of enumerated values, one for each view desired, and the enum conforms to NavigationDestination, which in turn conforms to View, so the view body can live inside the enumeration.

swift
nonisolated public enum HomeDestinations: NavigationDestination {

    case page2
    case page3
    case pageN(Int)
    
}

Each case then returns the correct view, and associated values carry parameters into that view.

swift
    case pageN(Int)
    
    public var body: some View {
        switch self {
        case .page2:
            HomePage2View()
        case .page3:
            HomePage3View()
        case .pageN(let value):
            HomePageNView(number: value)
        }
    }

SwiftUI requires navigation destination values to be Hashable, and Navigator holds to the same rule, so a destination enum is a closed, typed set of screens rather than a stringly typed route. The README notes that views with external dependencies or needing environmental values are handled by a separate section on advanced destinations, which is the usual place where the simple pattern stops being enough.

ManagedNavigationStack replaces NavigationStack

The migration from a stock NavigationStack is one substitution.

swift
struct RootView: View {
    var body: some View {
        ManagedNavigationStack {
            List {
                NavigationLink(to: HomeDestinations.page3) {
                    Text("Link to Page 3!")
                }
            }
        }
    }
}

ManagedNavigationStack creates a NavigationStack for you and installs the associated Navigator environment variable that manages that particular stack. It provides it with the NavigationPath and also supports navigation options such as automatically presenting sheets and fullScreenCovers. Since 2.1.2, NavigationLink also accepts a title directly alongside the destination, so the trailing closure can be skipped for a text-only link. The title argument accepts a plain string, a LocalizedStringKey or a LocalizedStringResource, which means localized titles work without extra ceremony. The other thing ManagedNavigationStack gives you is scope. Each stack has its own Navigator environment value, so a tab-based app can have a navigation hierarchy per tab rather than one global path, which is where the separation of concerns in the feature list starts to matter.

How the registration trick works under the hood

The README is candid that the mechanism looks like black magic and then explains it in one sentence. Navigator provides an initializer for NavigationLink that takes NavigationDestination types and maps them to an internal type that ManagedNavigationStack has already registered for you. Because a single navigationDestination handler can then push NavigationDestination views of any type, a modular application whose home screen shows cards from different modules can push an internal view from the module that owns the card, without that module's types ever being registered at the root. Before version 1.2.0 the modifier form .navigationDestination(HomeDestinations.self) was the way, and as of 1.2.0 there is no need for it. The catch is stated in bold-adjacent prose: this is potentially a breaking change, and using the old NavigationLink(value:label) view without defining the destination will make navigation fail. The mitigation is offered too. If you need the registration mechanism to support older code, keep using NavigationLink(value:label) with the usual navigationDestination registrations.

Declarative and imperative navigation side by side

Destinations can also be dispatched programmatically through Navigator, or declaratively using modifiers, and the two styles share the same destination types. The declarative form uses an optional destination in state and a navigate modifier bound to it.

swift
@State var page: SettingsDestinations?
Button("Modifier Navigate to Page 3!") {
    page = .page3
}
.navigate(to: $page)

The trigger-value variant is the other half, where a change in a trigger value causes the navigation rather than an explicit assignment. The important property is that both paths go through the same typed destination enums, so a deep link, a programmatic push and a tapped link all end at the same screen with the same arguments. That shared vocabulary is also what makes the cross-module claim work, since a module can publish a destination enum that another module pushes without either side registering anything at the root.

Checkpoints, deep links and state restoration

Three of the advertised features go beyond what NavigationStack gives you for free. Navigation checkpoints let you return to a specific spot in the navigation tree rather than popping one level at a time, and they can carry callback values back, so a flow that started in one screen can return a result to it. Deep linking is described as true deep linking and internal application navigation via navigation send, which is the mechanism for jumping to a location inside the app from outside it. Navigation state restoration means the path can be saved and restored, so the app comes back where the user left it rather than at the root. Event logging and debugging round out the list, and that one is easy to underrate. A navigation layer that records what it did turns an intermittent wrong-screen bug into a question you can answer from a log, rather than a question you answer by adding print statements to every view.

Where Navigator is the wrong choice

The scope is the first limit. Navigator targets iOS 17 and above, which excludes any app still supporting iOS 15 or 16, and its API leans on Observation and modern SwiftUI, so an older codebase with a heavy UIKit shell is a poor fit. The second limit is the cost of the abstraction. You give up SwiftUI's own NavigationLink and NavigationStack API and learn a parallel one, and the README is upfront that the migration is potentially breaking, with the old link form failing silently in the sense that navigation simply does not happen. In a large codebase, that means auditing every NavigationLink, not just adding a dependency. The third limit is that the visible part of the documentation is the easy part. The README demonstrates the destination enum, the managed stack, the registration trick and the navigate modifier, and it points to advanced destinations for views with external dependencies or environmental values without walking through them, so the hard cases are exactly the ones you would want explained before adopting. The fourth is dependency scope: this is a Swift package that assumes it can sit inside your dependency graph and shape how every screen is presented, which is a deeper coupling than a utility library.

Editorial conclusion

Adopt Navigator if your SwiftUI app has grown past a single NavigationStack and you are tired of registering destination types, especially if you need deep links or a way back to a specific screen. Skip it if your app has a handful of screens, since the plain NavigationStack already handles that. Before you migrate, change every NavigationLink(value:label) to NavigationLink(to:label), because the README warns that using the old form without defining the destination makes navigation fail.

Frequently asked questions

What does the Navigator SwiftUI package do?

It provides a SwiftUI navigation layer based on NavigationStack with typed destination enums that declare their own views, plus coordination patterns, cross-module navigation, deep linking, navigation checkpoints, declarative and imperative navigation, state restoration and event logging. Version 2.1.4 runs on iOS 17 and above.

Do I still need navigationDestination registrations with Navigator?

No. Navigator provides a NavigationLink initializer that takes NavigationDestination types and maps them to an internal type ManagedNavigationStack has already registered, so a single handler can push NavigationDestination views of any type. Use `NavigationLink(to:label)`; using the old `NavigationLink(value:label)` without defining the destination makes navigation fail.

What are navigation checkpoints in Navigator?

Checkpoints let you return to a specific spot in the navigation tree rather than popping one level at a time, and they can return callback values to the point where the flow was started.

What is the difference between ManagedNavigationStack and NavigationStack?

ManagedNavigationStack creates a NavigationStack for you and installs the Navigator environment variable that manages that stack, giving it the NavigationPath and supporting options such as automatically presenting sheets and fullScreenCovers. The README describes the swap as a simple substitution where you once used NavigationStack.

Official sources

  1. hmlongco/Navigator on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/hmlongco-navigator.svg)](https://hysenlabs.com/projects/hmlongco-navigator)

Was this useful?