Why messy?
It's because of the core idea of SwiftUI — a view is a function of the state, or a view is state-driven. Don't get me wrong, this concept is great, but SwiftUI's navigation is not this advanced yet.
However, SwiftUI does not have the means to construct robust navigation inside your app.
Messy example
Consider the common case of the onboarding screen when you need to present some sequence of views with nice transitions. What can you do with SwiftUI? Probably, create an enum
that tells which screen is active and then use switch
to present the sequence of views.
What if you need to modify the order or change the number of views? You'll need to modify the corresponding enum
, modify the logic of switching inside the views, and other stuff.
Not so flexible, right?
Oh, and then you decide to present one view right in the middle through .sheet
. That's when the mess starts to show up. You create an additional @State
to check if the sheet is open, make sure that it's updated correctly, and restructure the switch
block that you used before.
Now, it's a chaotic view that is prone to unexpected bugs.
Existing navigation views
The most obvious one is NavigationView which is deprecated in the new iOS 16.

Using NavigationLink
, it can present new views and also adds a "back" button to return to the previous view.
And it does not support programmatic navigation.
Apple presented a new NavigationStack that addresses this issue but it is still not flexible enough. For example, I like to have the ability to modify the view whatever I want, but NavugationStack inserts back buttons. Also, it does not support different transitions. While it is nice to see SwiftUI develop in this direction, yet we are not there.
So, even in iOS 16, SwiftUI is not powerful enough to manage any kind of navigation you can come up with.
And .sheet()
. NavigationStack
does not make it easier to handle .sheet()
either.
Designing a flexible navigation library
I decided to create a library with several requirements:
- Programmatic views navigation
- Ability to present a sequence of views
- Support for any SwiftUI transition and Animation
- Completely state-driven: no singletons or environment objects
- Handle
.sheet()
Sounds cool, right?
Straight to the point, I was able to create such a library.
So, if you just want a nice tool for the things I listed above, you can stop here. Now, let's see how I did it.
Ways to present
At the core of the library is a structure that stores views and information about how to present them. Possible options for presentation are
enum PathType {
/**
* Just show a view. No animation, no transition.
* Show view above all other views
*/
case plain
/**
* Show view with in and out transitions.
* Transition animation also can be specified.
*/
case animated(transition: AnyTransition, animation: Animation)
/**
* Show view in .sheet()
*/
case sheet(onDismiss: Action)
}
.sheet()
is as easy as just presenting any other view.So, you can present the view without any animation, present it with needed transitions, and present it in a sheet.
Path
This structure stores information about views. It just stores an array of type-erased views with presentation type information. You can append views on top and remove them from the top.
Honestly, I got this Idea from NavigationStack
as previously I tried to do a similar library with the ability to insert in the middle. However, I encountered several issues concerning animation when inserting it in the middle. Probably, it's possible to do it.
The view itself
The key idea is how to present this array of views. PathPresenter uses ZStack to do that. It presents only views that are not marked as .sheet
type.
ZStack(alignment: .topLeading) {
Color.clear
if let rootView = rootView, !sheet {
rootView.zIndex(-1)
}
ForEach(content, id: \.hashValue) { elem in
switch elem {
case .plain(let view, hash: _, zIndex: let zIndex):
view.zIndex(zIndex)
case .animated(
let view,
transition: let transition,
animation: _,
hash: _,
zIndex: let zIndex):
view.zIndex(zIndex).transition(transition)
case .sheet(let view, _, _):
view
}
}
}
The view manages .sheet
inside of itself and decides when it needs to be presented. If the last element must be presented as a sheet, then the sheet is activated.
That's it
I covered the core concepts of the implementation. You can check GitHub and see the full implementation. The code is fully documented and you can ask me about anything in the comments.
Onboarding example with PathPresenter
We simply use the library ;)
To construct the Path, we append needed views to it:
let data = [
(text: "Nice", subtext: "Onborading sequence of screens"),
(text: "OK", subtext: "But how to do it with SwiftUI?"),
(text: "So that", subtext: "it is nice and shiny"),
]
let typeCommon: PathPresenter.PathType =
.animated(
transition: .asymmetric(
insertion: .move(edge: .trailing),
removal: .move(edge: .leading)),
animation: .easeInOut
)
path.append(
data: data,
type: typeCommon
) { (text, subtext) in
boldText(
text: text,
subtext: subtext
)
}
path.append(
boldText(
text: "And also",
subtext: "flexible enough to cover all your needs"
), type: .sheet(onDismiss: {}))
path.append(
boldText(
text: "Read the post",
subtext: "to find the solution"
), type: typeCommon)
path.reverse()
That's it. Then, you use RoutingView(path: $path)
to present this path. You can check out the full example in this project:
There are also similar frameworks available on GitHub. Recently I discovered quite a nice one:
One Step Further
Actually, navigation implementation is part of the bigger picture as it lies in the core of app's architecture. Therefore, check out my articles on modulized app architecture.



Final notes
I really like how this library turned out. You freely can construct any sequence of views and build your own navigation.
Do not hesitate to contact me if you have noticed any bugs.