SwiftUI Navigation Is a Mess. Here’s What You Can Do

Managing navigation in pure SwiftUI is hard and leads to messy solutions. In this post, I will show you how you can manage views effectively

SwiftUI Navigation Is a Mess. Here’s What You Can Do
Photo by Mick Haupt / Unsplash

SwiftUI is nice and powerfil. Yet, managing navigation in pure SwiftUI is hard and leads to messy solutions. In this post, I will show you how you can manage views effectively.

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.

💡
The view is a function of the state and navigation is not an exception

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.

0:00
/

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.

Image by https://developer.apple.com/documentation/swiftui/navigationview

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.

GitHub - AlexRoar/PathPresenter: Pure SwiftUI state-driven library to present view sequences and hierarchies.
Pure SwiftUI state-driven library to present view sequences and hierarchies. - GitHub - AlexRoar/PathPresenter: Pure SwiftUI state-driven library to present view sequences and hierarchies.
💡
I am always open to objective criticism and requests for a new feature. Do not hesitate to open an issue on GitHub!

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.

Subscribe and don't miss posts!

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)
}
Note that presenting through .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:

GitHub - AlexRoar/PathPresenterExample
Contribute to AlexRoar/PathPresenterExample development by creating an account on GitHub.

There are also similar frameworks available on GitHub. Recently I discovered quite a nice one:

GitHub - johnpatrickmorgan/FlowStacks: FlowStacks allows you to hoist SwiftUI navigation and presentation state into a Coordinator
FlowStacks allows you to hoist SwiftUI navigation and presentation state into a Coordinator - GitHub - johnpatrickmorgan/FlowStacks: FlowStacks allows you to hoist SwiftUI navigation and presentati...

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.

Subscribe to Alex Dremov

Don’t miss out the latest publications! Only useful posts for developers to bump up your professional skills
bestdeveloper@example.com
Jump In