How to make Navigation in SwiftUI a piece of cake

Karin Prater
4 min readJun 21, 2020

If you are used to functional programming with UIKit, it takes some time to wrap your head around SwiftUI. This is especially true for navigation. Storyboards and Navigation Segues are just so essential. But after a couple of months I have finally got the hang of it. So I will share some very useful design patterns and SwiftUI bugs to avoid with you.

This is not a beginner tutorial. You will need to review NavigationView, TabView, and SplitViews. With iOS 16 you can also use NavigationStack and NavigationSplitView which are more reliable. All of these navigation elements are working with state, which I will explain to you in this post.

What is the state of Navigation?

SwiftUI is based on declaration programming. Everything is state driven. Also the state of your app. The state can be defined on the tabView selected, the NavigationLinks and the states to show modal views and popovers. Let’s see an example. Creating a new project with a Tabbed App, gives us this ContentView:

The var defining the state is the selection in line 4. It is either 0 for “first view” or 1 for “second view”.

If now look at NavigationView with NavigationLink, the state is defined by the isActive of the link:

If you are using NavigationLink(destination: , label:) SwiftUI will keep the state var hidden from you. But it is still there. Next example is alert, where again we use @State to mange what views are shown.

The class that rules them all

In the beginning I was handling them around from view to view, until I realized that I want to keep them together. Keeping things nice and tidy. So here is what I came up with:

This class needs to use ObservableObject, so I can use it in the environment. In order to place it in the environment and access my NavigationController vars anywhere, we will place it at the very top most view level. That is in the SceneDelegate. This works in multi window, because each window has it’s own instance of SceneDelegate and thus it’s own NavigationController. Changing SceneDelegate to:

We now need to bind the navigation views to the NavigationController, so it holds all the information. This means we need to bind the selection of TabView and the NavigationLink active State.

Creating shortcuts for the user

After all this work, we can now include some handy little features. Let’s say we have a button on our detailView that should bring the user to section “Second”.

As an example you might have a subscription model included in your app. The user wants to unlock the pro feature and you want to bring them to the subscription section of your app. So easy for you now.

Collapsing NavigationStacks

One thing that is very different to UIKit and Storyboards is how you collapse navigationstacks. In our example we just want to go back from FirstDetailView to FirstView, we set the detailIsShown to false. If you have navigated through multiple views, just set them all to false.

Reacting to the backend

So now we advance to a more complex use cases. In my app Assocy I am showing documents. When the user logs out, I need to update the UI and close open documents. This is necessary because I am supporting multi windows on iPad and I will need that for the macOS version later.

There is a lot to discuss in the following code snippit. So I will highlight the most important design patterns. The login state is managed by a singleton class AuthSubscriptionManager. Since I want to access the same information from multiple windows, I do not want to use an EnvironmentObject. The singleton pattern gives me one AuthSubManager instance for the whole app. The login is done with FirebaseAuth. When the var firebaseUser changes, I want to update my UI. In swiftUI this is easily done with Combine (I am not a fan of NotificationCenter, but it would work, too). So in the NavigationController we subscribe to any changes to the firebase user (line 13). When this happens we call resetAll(), where the navigation state vars are set back. You can update the UI state depending on your wishes.

Take the user right back

So fare every time the app is opened the user is taken to the same view, which we specify during the init of NavigationController. What if we wanted to dynamically choose the launch state? For example, we would only want to show the Onboarding the first time. In NavigationController we use UserDefaults to store the onboarding state.

In the main content view, we check if the user has seen the onboarding before. If not we show the OnboardingView. When the user taps on the “done” button we change the hasSeenOnboarding to true, which is saved in Userdefaults.

You can use this pattern to control a whole onboarding flow. I also use this pattern to ask the user to review your app after x-number of app launches.

Some problems that need mentioning

SwiftUI is still not as stable as I hoped at this point. One common bug occures with the Environment. If you have NavigationLinks, the environment gets lost in some cases. To prevent this, you need to explicitly pass the environment in the NavigationLink like so:

Take away

I hope you have a better understanding of the state driven design pattern that SwiftUI uses. Once you understand this, it is quite straight forward and at least for me a bit addicting. I look back on navigation segues in UIKit and don’t miss the amount of code necessary at all :).

--

--

Karin Prater

I have a Ph.D. in physics, during which I started to love software engineering. I have deveped my own app with SwiftUI and I created a SwiftUI online course.