# NavigationBackport: SwiftUI NavigationStack on older operating systems

> NavigationBackport recreates the SwiftUI navigation APIs introduced at WWDC22 on top of NavigationView and NavigationLink, so a project can adopt NavigationStack style code while still targeting older iOS, tvOS, macOS and watchOS versions, then delete the dependency when the deployment target moves. The naming is prefixed, the path can be an array or a navigation path, and deep links are made to work by splitting one state update into several.

**johnpatrickmorgan/NavigationBackport** — Backported SwiftUI navigation APIs introduced in WWDC22

- Repository: https://github.com/johnpatrickmorgan/NavigationBackport
- Stars: 914 · Forks: 59
- Language: Swift
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/johnpatrickmorgan-navigationbackport

## An API mirror with a prefix, not a reimplementation

NavigationBackport is a Swift package that uses the navigation APIs available in older SwiftUI versions, such as NavigationView and NavigationLink, to recreate the new NavigationStack APIs introduced in WWDC22. The purpose is to let a project start targeting those APIs while still supporting older versions of iOS, tvOS, macOS and watchOS.

The mapping is a straight rename with a prefix, and the README lists it plainly.

```swift
NBNavigationStack
NBNavigationLink
NBNavigationPath
nbNavigationDestination
NBNavigationPath.CodableRepresentation
```

What happens under the hood matters more than the names. When the package is running on an operating system version that supports NavigationStack, NavigationStack is used under the hood. On older systems it falls back to the older primitives. That means the behaviour of your app can differ subtly between OS versions, and the prefix is what keeps the two paths distinguishable in your source while you are in the transition.

The navigationDestination modifier becomes the nbNavigationDestination modifier rather than a new type, because it is a View modifier, and the CodableRepresentation nested type is mirrored on the backported path type so that serialised navigation state keeps working.

## The migration path is the product

The stated plan is that you can migrate to these APIs now, and when you eventually bump your deployment target you can remove the library and easily migrate to its SwiftUI equivalent.

That framing explains the whole design. The package is not trying to be a better navigation system than SwiftUI's, it is trying to be a temporary shim that lets a team adopt the new API before it can support the new operating systems. The NB prefix is the cost of that shim: it is greppable, it is mechanical to remove, and it is unpleasant on purpose.

There is a full replication of the NavigationStack API behind the prefix. An NBNavigationStack can be initialised with a binding to an Array, with a binding to an NBNavigationPath binding, or with no binding at all. That third case is the one worth pausing on, because it is the API you would use when the navigation stack is not driven by application state at all.

The library also adds things SwiftUI does not have, which the README describes as additional features: a Navigator available through the environment, and a set of navigation utility functions that work whether you are holding an Array, an NBNavigationPath or a Navigator.

The alternative to this approach is not another package but a different plan, which is to keep writing NavigationView and NavigationLink code until the deployment target moves and then rewrite it in one pass. The package exists for teams who would rather not do that rewrite twice.

## Arrays and paths, and a protocol that keeps arrays clean

The navigation functions are the part of the API that touches your data structures directly.

```swift
path.push(Profile(name: "John"))
path.pop()
path.popToRoot()
path.popTo(Profile.self)
```

Pushing, popping, popping to the root and popping to a specific screen by type are all available, and they work the same way whichever of the three backings you use.

There is a catch, and the README is precise about it. If you want these methods on an Array, the Array's element must conform to NBScreen, a protocol that inherits from Hashable without adding any additional requirements. The stated reason is that this avoids polluting all arrays with APIs specific to navigation.

That is a real design constraint with a real benefit. Swift extensions on Array are global, so adding push and pop to every array in your project would be a namespace collision waiting to happen, and it would affect arrays that have nothing to do with navigation. Constraining the behaviour to a marker protocol keeps the extension scoped to arrays you have opted in.

The cost is that the element type has to be Hashable, and that a shared model type used for more than one purpose may need to conform to it.

## A navigator in the environment, generic over your screen type

The Navigator is an object available through the environment that gives access to the current navigation path.

For a stack backed by an NBNavigationPath, the environment object is a PathNavigator.

```swift
@EnvironmentObject var navigator: PathNavigator
```

For a stack backed by an Array, it is a generic Navigator over your screen type.

```swift
@EnvironmentObject var navigator: Navigator<ScreenType>
```

Two things are going on here. The first is inspection: the navigator exposes the current path elements, so a view can react to where the user is rather than only to where they are going. The second is control: the navigator can push new screens, pop, pop to a specific screen, or pop to the root.

The generic form is the more interesting of the two, because it means the navigator is typed by the screens your app actually has, and the environment object is set up by the stack that owns it. A view can then say popTo(Profile.self) and have it mean something at compile time rather than at runtime.

In the example in the README, a NumberView holds the navigator as an environment object and uses it to pop to the root from a button, which is the typical case where a deep stack needs an escape hatch.

## Deep linking works by splitting one state update into several

This is the most interesting piece of engineering in the package, and the README describes it as a workaround rather than a feature.

Before NavigationStack, SwiftUI did not support pushing more than one screen in a single state update, which is exactly what happens when you deep link to a screen several layers down a hierarchy. NavigationBackport works around the limitation: you can make any such path changes, and the library breaks the larger update down into a series of smaller updates that SwiftUI supports, with delays in between.

```swift
  path.append(Screen.orders)
  path.append(Screen.editOrder(id: id))
  path.append(Screen.confirmChanges(orderId: id))
```

That code pushes three screens in one state update. On a system with NavigationStack, all three are pushed in a single update. On an older system, the library pushes them one by one, with the delays in between to let SwiftUI catch up.

The important detail is the last sentence of that section. It only happens when necessary, so on modern systems the behaviour is the native one and the workaround does not get in the way.

The cost of the workaround is latency and a visible transition on old systems, since the user sees the screens arrive in sequence. For a deep link into a multi-screen flow, that is the price of supporting the older platform, and it is much cheaper than the alternative of not supporting deep links at all on those systems.

## iOS 13 support lives on a separate branch

The library targets iOS and tvOS versions 14 and above, and the reason given is concrete: it uses StateObject, which is unavailable on iOS and tvOS 13.

For anyone who still needs those two versions, there is an ios13 branch which uses the backported StateObject from SwiftUIBackports, so that it works there too. The README links that project directly, which is the kind of detail that saves an afternoon of searching.

The practical guidance that follows from this is worth stating plainly. If your app supports iOS 13, you are adopting a branch rather than a tagged release, and you need to decide how you will track it. The project publishes tagged versions, and the main branch is where the current work lands, so an app that must support iOS 13 is pinning to a branch that will move under it, unless it pins a tag.

There is also a related project for people who want to go further. FlowStacks enhances these familiar APIs to allow you to additionally drive sheet and full-screen cover navigation from a single unified interface, so navigation and presentation live in one model rather than two.

The repository layout shows a standard Swift package with Package.swift, a Sources directory, a Tests directory, and a .swiftpm directory. There is also a NavigationBackport.podspec, so the package can be consumed through CocoaPods as well as Swift Package Manager, and a NavigationBackportApp directory which holds a demo application.

## Maintenance pace and the shape of the package

The release history shows a project that keeps moving but does not churn.

The tagged versions are 0.11.6 from 2026-07-03, 0.11.4 from 2025-06-27 and 0.9.3 from 2024-04-17. So there were four years between the 0.9 line and the 0.11 line, and the last push was on 2026-07-03, the same day as the newest tag.

The pace suits what this package is. It is a compatibility layer for an API that Apple ships natively now, so the work is maintenance rather than feature development: keeping up with SwiftUI changes, fixing bugs on older operating systems, and keeping the API surface in step with the system so that the removal path stays easy.

The licence is MIT, with the text in the LICENSE file at the root. The package uses Swift Package Manager as its primary distribution, with a CocoaPods podspec alongside, and there is a .swift-format.json at the root, so the source is formatted with a checked-in configuration.

The author also runs a Ko-fi link in the README, which is a small signal about how the project is funded and about how much of the work is solo.

## Conclusion

Adopt NavigationBackport when your app still supports an operating system version without NavigationStack but you want to write navigation code in the modern style, and when the rename to NB-prefixed symbols is acceptable in your codebase. Do not adopt it if your minimum deployment target already supports NavigationStack, because then you gain nothing over the system API, and be aware that the naming cost is permanent in the source even though the dependency is temporary. Before the first build, check the deployment targets of your app, iOS and tvOS 14 are the floor because the package uses StateObject, and plan the removal: when you raise the deployment target, replace NBNavigationStack, NBNavigationLink, NBNavigationPath and nbNavigationDestination with their SwiftUI equivalents and delete the dependency.

## FAQ

### What does NavigationBackport actually do?

It uses the older SwiftUI navigation APIs such as NavigationView and NavigationLink to recreate the NavigationStack APIs introduced at WWDC22, so a project can use the new style while still supporting older iOS, tvOS, macOS and watchOS versions. On systems that support NavigationStack, it is used under the hood.

### How do I remove NavigationBackport later?

The APIs are prefixed, so the removal is a rename: NBNavigationStack becomes NavigationStack, NBNavigationLink becomes NavigationLink, NBNavigationPath becomes NavigationPath and nbNavigationDestination becomes navigationDestination. The project is designed to be deleted once you raise your deployment target.

### Does NavigationBackport support iOS 13?

The library targets iOS and tvOS 14 and above because it uses StateObject, which is unavailable on iOS and tvOS 13. There is an ios13 branch that uses the backported StateObject from SwiftUIBackports so that it works on those versions too.

### How do I push, pop and navigate to a specific screen with NavigationBackport?

Utility functions are available on an Array, an NBNavigationPath or a Navigator, covering path.push, path.pop, path.popToRoot and path.popTo with a type such as Profile.self. To use them on an Array, the element type must conform to NBScreen, a protocol that inherits from Hashable without extra requirements.

### How does NavigationBackport handle deep links to a screen several levels deep?

SwiftUI before NavigationStack could not push more than one screen in a single state update, so the library breaks a large path change into a series of smaller supported updates with delays in between. On systems that support NavigationStack all the screens are pushed in one update instead.

## Sources

- [Issues](https://github.com/johnpatrickmorgan/NavigationBackport/issues)
- [johnpatrickmorgan/NavigationBackport on GitHub](https://github.com/johnpatrickmorgan/NavigationBackport)
- [License: MIT](https://github.com/johnpatrickmorgan/NavigationBackport/blob/main/LICENSE)
- [README](https://github.com/johnpatrickmorgan/NavigationBackport/blob/main/README.md)
- [Releases](https://github.com/johnpatrickmorgan/NavigationBackport/releases)

---

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