SwiftUI is undeniably great at many things. But after years of hands-on experimentation, we’ve learned that there are still areas where UIKit simply works better. One of those areas is navigation.
In this article, we focus specifically on app navigation. We’ve tried to replace UIKit-based navigation with SwiftUI multiple times during internal research. Each time, we ended up unsatisfied with the result. That’s why we still rely on a slightly evolved version of the coordinator-based solution originally described by Gleb in How to supercharge coordinators.
In the early days of SwiftUI, it was clear that it wasn’t production-ready for large-scale app navigation. The APIs were limited, fragile and hard to reason about once an app grew beyond a simple flow.
Things started to look more promising in 2022, when Apple introduced NavigationStack. At STRV, we took that as a signal to start experimenting seriously. Since then, we’ve explored four different SwiftUI-based navigation approaches internally. None of them met our standards.
The Main Issues With SwiftUI Navigation
1. NavigationStack Can’t Be Split Across Coordinators
One of the biggest problems is that navigation inside a NavigationStack can’t be easily split across multiple coordinators.
Imagine a Profile screen that can be pushed from several places in the app. Architecturally, it makes sense to have a dedicated ProfileCoordinator.
With UIKit, this is trivial. You pass a reference to an existing UINavigationController into the coordinator and push from there.
With SwiftUI, this model breaks down. Navigation is driven by state, which makes it hard to reuse flows across multiple entry points. You either duplicate logic or build complex abstractions on top.
2. There's No Presentation or Dismissal Completion
UIKit provides completion handlers for presentations and dismissals. You know exactly when a transition finishes. SwiftUI has no equivalent.
Once a presentation is triggered by a state change, there’s no direct way to tell when a screen has fully appeared or when it has been dismissed. The workaround is to add extra state to a coordinator and infer lifecycle events indirectly using onAppear and onDisappear.
Without access to this information at the point where navigation state changes, coordinators are forced into more complex coordination via state and lifecycle callbacks.
3. Custom Presentations and Transitions Are Limited
Custom presentation styles and transitions that are trivial to implement in UIKit are limited in SwiftUI navigation.
This includes things like custom modal presentations, cross-dissolves or fine-grained control over navigation transitions. Achieving these effects in SwiftUI usually requires indirect workarounds. The resulting solution is harder to reason about and maintain.
4. Mixing SwiftUI and UIKit Is Asymmetrical
Presenting SwiftUI from UIKit is easy thanks to UIHostingController.
Going the other way is worse. Presenting a UIKit screen from SwiftUI requires wrapping view controllers in UIViewControllerRepresentable, which adds boilerplate and complexity.
We Tried to Fix It
We were able to find workarounds for all of these issues.
To address these limitations, we engineered two distinct solutions. You can explore both experiments in our SwiftUI navigation examples repo. Both attempts revealed fundamental flaws.
Solution 1: Custom wrappers around NavigationStack. The first solution became so complex that it was unreliable and hard to maintain.
To bridge the gap between our imperative coordinators and SwiftUI, we built two custom components: NavigationStackWrapper to encapsulate the native stack and control the path, and CoordinatedViewAdapter to act as the middleman binding our logic to the view hierarchy.
We produced a working prototype. The cost was visible in the architecture: complicated dependencies between NavigationStackWrapper and CoordinatedViewAdapter. To manage the flow, we relied on PassthroughSubject to signal navigation events, but observing these signals proved inconsistent. We hit timing issues where the view hierarchy wasn't ready to receive an update, which forced us to introduce async delay hacks just to ensure transitions completed.
It was extensive "glue code" to replicate behavior UINavigationController handles natively. This experiment proved that for complex apps, the framework requires too many unstable workarounds to be viable.
Solution 2: A SwiftUI API that became UIKit. The second solution resulted in an API almost identical to UIKit. We started in pure SwiftUI, then iterated the solution until the API naturally converged on UIKit semantics: controllers you pass by reference, push/pop and present/dismiss. At that point, it was clear we’d rebuilt UIKit on top of SwiftUI. So we stopped. There was no real benefit to keeping the SwiftUI wrapper over just using UIKit directly.
Navigation is not an area where “mostly works” is acceptable. It has to work 100% of the time.
The Hard Reality About SwiftUI Navigation
Today, SwiftUI offers no real advantage when it comes to navigation in larger, long-lived applications.
It absolutely shines as a UI-building tool. Lists, grids, animations and responsive layouts are faster to implement and easier to maintain. But navigation, the backbone of any real-world app, is where SwiftUI still falls short once complexity grows.
Adopting SwiftUI navigation just to be “more modern” doesn’t buy you anything.
The biggest risk isn’t that SwiftUI navigation fails outright. The real risk is that you don’t realize it’s failing until months later, when the app grows, edge cases pile up and rewriting navigation back to UIKit becomes a major and expensive effort.
How to Combine SwiftUI and UIKit for App Navigation
Based on our experience, a pragmatic approach works best:
- Use SwiftUI for individual screens and view composition.
- Use UIKit (and coordinators) to own navigation and flow.
- Treat SwiftUI as a powerful rendering layer, not a navigation framework.
This approach gives you the best of both worlds: fast UI development with SwiftUI and predictable, battle-tested navigation with UIKit.
Should You Use UIKit or SwiftUI for Navigation?
After years of experimentation, here's our take:
- SwiftUI is a great layout engine.
- SwiftUI navigation can work for small apps, but UIKit is still the most reliable choice for navigation in complex applications.
Replacing UIKit coordinators with SwiftUI purely for the sake of modernization is a trap. Until SwiftUI introduces something fundamentally better for navigation, UIKit will continue to own this space.





