swift-navigation: the library that turns three optionals into one enum
Bringing simple and powerful navigation tools to all Swift platforms, inspired by SwiftUI.
At a glance
- What is it?
- Point-Free's SwiftNavigation is a small package whose job is to make enum-driven navigation compile-time safe in SwiftUI and UIKit. The interesting parts are in what it deliberately leaves to you.
- Who is it for?
- SwiftNavigation is worth adopting if you already model navigation as an enum and want the compiler to hold the line on it, or if you want the two-way binding layer that drives UIKit from the same model. It is a foundation library, not a navigation framework, and the distinction decides your expectations.
- 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 37 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 September 28, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A README that argues a point rather than lists features
The SwiftNavigation README does something unusual for a library of this size: it takes a single bug pattern and argues against it for several thousand characters. The setup is three optionals on an observable model, one for a sheet, one for an alert, one for a drill-down.
@Observable
class FeatureModel {
var addItem: AddItemModel?
var deleteItemAlertIsPresented: Bool
var editItem: EditItemModel?
}The argument is arithmetic rather than aesthetic. Three optionals describe eight states, four of which are legitimate: everything nil, or exactly one of the three set. The other four can tell SwiftUI to present a sheet and an alert simultaneously, which SwiftUI permits today and warns about in the console as a future crash. A reader who has not hit this will recognise it immediately, and a reader who has hit it will recognise the warning text.
The enum is the fix, and the library is what makes it drive the UI
The README then does what it promised by the title of the problem. The three optionals collapse into one optional of an enum.
@Observable
class FeatureModel {
var destination: Destination?
enum Destination {
case addItem(AddItemModel)
case deleteItemAlert
case editItem(EditItemModel)
}
}At that point the model is correct and the app is broken. SwiftUI has no way to present a sheet from a case of an enum, which is the gap SwiftUINavigation fills. The mechanism is a macro on the enum.
+import SwiftNavigation
+
+@CasePathable
enum Destination {
// ...
}With @CasePathable attached, each case becomes reachable through a key path, and the library synthesises bindings from those key paths so the original view modifiers keep working.
import SwiftUINavigation
// ...
.sheet(item: $model.destination.addItem) { addItemModel in
AddItemView(model: addItemModel)
}
.alert("Delete?", isPresented: Binding($model.destination.deleteItemAlert)) {
Button("Yes", role: .destructive) { /* ... */ }
Button("No", role: .cancel) {}
}
.navigationDestination(item: $model.destination.editItem) { editItemModel in
EditItemView(model: editItemModel)
}The net effect is that the diff you write to fix a real class of bug leaves your view code almost untouched. That is the design argument the library is making.
The installation callout contradicts the import line
Here is the first thing that will cost you an hour if nobody warned you. The README places an important callout directly above the first macro example.
> [!IMPORTANT] > To get access to the tools described below you must depend on the SwiftNavigation package (see Installation) and import the SwiftUINavigation library.
Both halves of that instruction are stated, and they describe different things. You add the SwiftNavigation package as your dependency, because SwiftUINavigation is a library inside that package rather than a separate package you fetch. Then you import SwiftUINavigation, the module, not SwiftNavigation, the package. The macro example reinforces it with a line reading import SwiftNavigation followed by @CasePathable, and the later example reads import SwiftUINavigation. So the same README shows both names, in the same section, attached to the same feature.
Both readings are consistent with how Swift Package Manager works and the second one is the one that compiles. The confusing part is that the callout is telling the truth about the package while the code sample sits next to it showing a different import, and a reader who copies the first import they see gets a symbol not found error with no hint in the message.
The actionable version: add one package, import SwiftUINavigation for SwiftUI work and UIKitNavigation for UIKit work, and expect to see the package name in your dependency graph rather than your import statements. Nothing in the tree contradicts this, since Sources holds the modules rather than the package, but the README never states which module owns @CasePathable, so if you are writing an import list by hand from a memory of an older tutorial, check it against the tree first.
Two tools, four concepts, and one erased word
The README's own summary lists the primary tools as observe and UIBinding, then names supplementary concepts, UITransaction for associating animations and other data with state changes, and UINavigationPath as a type-erased stack of data. That split tells you where to look. observe and UIBinding are the load-bearing pair, and the other two are building blocks for the higher-level libraries.
UINavigationPath is the one with the widest reach outside Apple frameworks, because a type-erased stack of data is exactly what a URL router needs. If you are mapping incoming links onto a navigation stack, that type is the piece you would reach for, and the README's description of it is deliberately abstract, which is the tell that it is meant to be wrapped rather than used directly.
The package also positions itself as the foundation for sibling libraries rather than as a replacement for framework navigation. The README says SwiftNavigation forms the foundation that more advanced tools are built upon, and lists UIKitNavigation and SwiftUINavigation as those tools, while the installation section tells you to import one of them. Nothing in the tree contradicts the description of the package as foundation rather than facade, and the four manifests in the root are the structural evidence that there is real platform-specific work to do.
Package.swift
[email protected]
[email protected]
[email protected]Four manifests is the tell for how wide the platform support has become. A package that must vary its dependency graph by compiler version, rather than only its language settings, is one whose conditional surface is real. This is the kind of thing that decides whether a library can be adopted on a codebase still on an older toolchain, and the README does not mention it, so read the manifest nearest your own compiler before assuming parity.
The Makefile reveals how this is actually verified
The repository root carries a Makefile, and it is more informative about testing than the README is. It pins simulator versions to iOS 18.5, tvOS 18.5, and watchOS 11.5, sets a resilience compile flag on every invocation, and fans out into five test targets.
test: test-ios test-macos test-tvos test-watchos test-examplesEach platform target warms a simulator, then runs two xcodebuild invocations: one against the SwiftNavigation scheme for tests, one against a DynamicFramework scheme that builds only. The second invocation existing for all four platforms is the notable part. A library that publishes dynamic framework builds alongside static Swift Package Manager products needs those builds to keep compiling across every supported OS, and a tests-only pipeline would not catch a failure there.
The fifth target, test-examples, runs the CaseStudies scheme. That is the README's example code compiled and run rather than left to rot, which is worth more than any statement about example quality, and it is invisible unless you read the Makefile.
The resilience flag is worth a second look too. Libraries that install global runtime behaviour, and this one ships observable machinery that reaches into SwiftUI's environment, need to answer to tools that audit that behaviour. Enabling that check across every platform target is a reasonable default rather than an unusual one, but it does tell you the maintainers expect their macros to be inspected by something outside the project.
Three releases in five weeks, and the third one moves the mechanism
The release history is short and pointed. Version 2.11.0 shipped on 2026-07-30 and the notes name one feature, CaseBindable support for Perception.Bindable, credited to an outside contributor, plus a documentation badge on the README. Version 2.11.1 followed on 2026-08-28 with a single infrastructure change, a bump to support IssueReporting 2.1. Version 2.11.2 landed on 2026-08-31 with one line as well, and it is the one that matters for anyone planning an upgrade.
That line reads Infrastructure: Adopt CasePath protocol.
Read in sequence, the three releases tell a migration story. CaseBindable support means the library learned to interoperate with a third-party observation system, which is a compatibility decision rather than a feature in isolation. The IssueReporting bump is dependency maintenance on a package whose whole job is surfacing runtime problems, so keeping it aligned is how the library's own diagnostics stay legible. And adopting the CasePath protocol means the mechanism behind @CasePathable stopped being purely a macro convention and became an explicit protocol requirement.
For an existing user the last item is the one to check, because protocol adoption is where your own conformances would need to move. The release body does not state a migration step, which means either none is needed or it is documented elsewhere, and the honest answer is that the release notes alone will not settle it. The last push on the tree was 2026-09-04, which is recent relative to the 2.11.2 date, so the source in the tree is newer than the newest release and may already contain the follow-up work that the notes do not mention.
Deciding whether you need this at all
The library's case is strongest when your navigation is already an enum and your team keeps finding invalid-state bugs that a type system could have caught. In that case the enum refactor in the README is the main event and this library is the enabler, not the reason.
It is weaker in two situations worth naming. First, if your navigation is a single boolean, the whole problem the library solves does not exist and the cost is an extra dependency for nothing. Second, if you have deliberately chosen loose navigation, where any screen can push any other screen and the route is data rather than a closed set, an enum is the wrong shape and a type-erased path like UINavigationPath is the right one. The library provides that too, but the README spends nearly all of its length on the enum case, so reading only the README will tilt you toward it.
A third practical consideration is the observation layer. The README's example uses @Observable from the standard library, and 2.11.0 added interoperability with Perception.Bindable. That tells you the library is aware observation itself is contested ground in the Swift community. If your app has already standardised on one observation approach, confirm your choice is on the supported list before adopting, because the two-way binding story depends on it working together rather than alongside.
Editorial conclusion
SwiftNavigation is worth adopting if you already model navigation as an enum and want the compiler to hold the line on it, or if you want the two-way binding layer that drives UIKit from the same model. It is a foundation library, not a navigation framework, and the distinction decides your expectations. Two checks come first. Read the README's installation callout carefully, because the package you depend on and the module you import are different things, and the README makes you import a module that does not live in the package you installed. Then look at the 2.11.2 release, which adopts the CasePath protocol, and read what CasePath requires of you, because that is the mechanism underneath @CasePathable and it is where a migration would land. If your navigation is a boolean or a single optional, you gain nothing here and the enum refactor is the whole cost.
Frequently asked questions
How do I create a navigation stack in Swift?
Apple platforms ship NavigationStack in SwiftUI and UINavigationController in UIKit, and either can hold a path you drive from code rather than from view composition. This package adds UINavigationPath, a type-erased stack of data that you can own in a model and hand to either. The README teaches driving SwiftUI from an enum instead, which is a different shape: a closed set of destinations rather than an open stack.
What problem does SwiftNavigation actually solve?
It makes enum-driven navigation compile-time safe. Three optionals describe eight states when only four are valid, so the compiler cannot help you. An enum leaves one optional, and the library supplies the bindings that let SwiftUI and UIKit present the cases, so you keep the framework view modifiers instead of replacing them.
Which module do I import after adding the package?
The package you depend on is SwiftNavigation, and the module you import for SwiftUI work is SwiftUINavigation. For UIKit it is UIKitNavigation. The README's callout states this, but the macro example next to it shows a different import, so if you get a symbol not found error, check which name you imported before assuming the macro is broken.
What are observe and UIBinding used for?
observe is a way to minimally observe changes in a model, and UIBinding is two-way binding that connects navigation and UI components to an observable model. UITransaction and UINavigationPath are named as supplementary concepts that higher-level libraries build on, with UINavigationPath being a type-erased stack useful for routing and deep links.
What changed in version 2.11.2?
The release notes record a single infrastructure change: adoption of the CasePath protocol. That is the mechanism behind the @CasePathable macro, so anyone with custom conformances should read what the protocol requires before upgrading. Version 2.11.0 added CaseBindable support for Perception.Bindable and 2.11.1 bumped IssueReporting to 2.1.
Does swift-navigation work on platforms other than Apple ones?
The README names Windows, Linux, and Wasm alongside SwiftUI, UIKit, and AppKit, and the root holds four manifests named Package.swift plus [email protected], 6.1, and 6.2. Varying the manifest by compiler version rather than only by settings is the practical signal that the platform surface is real. Read the manifest nearest your toolchain to see what it actually enables.
Official sources
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.
[](https://hysenlabs.com/projects/pointfreeco-swift-navigation)