Navigating Among SwiftUI Views

Posted in SwiftUI

Updated on March 12th, 2021

⏱ Reading Time: 6 mins

Soon after someone comes to SwiftUI from UIKit, they come to realize that navigating among SwiftUI views is not the same as navigating among view controllers in UIKit. And if an app does not contain a navigation view or a tab view, how is navigation among SwiftUI views possible when those exist in the same hierarchy level?

This is exactly what I’m going to answer in this post. There are probably more than one solutions to that matter, but what I will present here is a combat-tested technique.

To get started, have a look at the following really simple views. They are quite common, as the only thing that gets changed on them is the background color and the displayed message.

Sample views

They also have a button in common, which presents a menu when pressed. Available options in that menu match to the three views, and the goal is to navigate to the respective one when tapping on any option.

Menu On

At the time being nothing works though, as all three views exist at the same hierarchy level and no way to navigate among them has been implemented yet.

The main idea behind the realization of a solution lies to the fact that any SwiftUI view can be embedded to any other SwiftUI view. Based on that, the above sample views can be contained to another view, with its only content being one of those three. Which one will be displayed at any given moment is something that will be determined on the fly depending on the value of a special property that will be existing to views’ environment. As a quick explanation, the environment is a short of a shared memory for SwiftUI views all the way down to the view hierarchy; all views can access objects added to it, get updates and modify them.

The RootView and the PresentedView type

With that said, the first move towards the implementation of that idea is to create the container SwiftUI view, preferably in a separate file. I like to call it the RootView; but the name is not really that important.

RootView

Before we do anything on the RootView’s body, it’s necessary to define a new custom type. Its purpose is to contain a single property only; the one that will be indicating the view that should be displayed by the RootView at any moment. Since that property should be synchronizing the user selection with the displayed content, we will mark it with the @Published property wrapper. That way, any changes made to its value will become known to the views that subscribe to them, and SwiftUI will be redrawing its content appropriately.

However, how exactly are we going to represent available views? The best way is to have an enumeration with cases representing them. Even more, that enumeration will be nested in the custom type that we’ll define right next.

I usually implement that custom type inside the RootView.swift file and before the RootView structure; that way I have everything gathered in one place. Using a different file for that purpose is not wrong either though.

With all the above being said, let’s define the custom type which we’ll call PresentedView and the inner enumeration, called AvailableViews:

The above cases red, green, and blue match to the RedView, GreenView and BlueView respectively. Also notice that PresentedView is conforming to the ObservableObject protocol.

Next, we’ll define the one and only property in the PresentedView class, which we’ll mark with the @Published property and name it currentView:

Note that the initial value matches to the view that we want to show first after the app is launched.

Making RootView the default view

Before we go the RootView body and implement the logic to display the proper view, it’s necessary to make a turn and go to the app’s structure (or the SceneDelegate, depending on how you’ve created the project). There, we’ll specify the RootView as the one to be initialized at app launch:

In addition to that, we’ll pass to the RootView’s environment an instance of the PresentedView type using the environmentObject() method. That instance will be accessible then by all views in the hierarchy, and any changes made to the currentView property will force SwiftUI update displayed content accordingly:

Finishing RootView implementation

With the above modifications in place, we can go back to the RootView. The first move is to prepare the view so it can receive changes occurred in the PresentedView object existing in the environment. We do so by declaring a new property and annotating it with the @EnvironmentObject property wrapper:

See that we don’t initialize a PresentedView instance; the above will make the presentedView property refer to the object already set in the environment.

Now, we can implement the RootView’s body properly. What we’ll do here is quite simple. Depending on the value of the currentView property in the presentedView object we’ll be initializing the matching SwiftUI view. We’ll do that in a switch statement:

Whenever a different view is selected to be displayed from now on, RootView will be showing the correct one.

Making menu options react to selection

There is just one last thing missing, and that is to enable menu so it reacts to selections properly. A menu is actually a picker view, and it exists in the SwiftUI file that implements the common appearance and behavior of all other views.

ColoredContentView

Since this view is meant to affect the displayed view through the menu options, it’s necessary to declare an @EnvironmentObject property as in the RootView:

The current menu implementation is the following, where a temporary binding value indicates the selection:

That has to be changed, and instead of the hardcoded binding value, we’ll provide as argument the binding value of the currentView property in the presentedView object:

In order for that property to be properly updated when a selection is made on the menu, each option must be tagged using the tag view modifier. The argument to pass is the AvailableViews case matching to the displayed text:

And that’s it! Every time an option in the menu is selected, the currentView property in the PresentedView instance existing in the views environment will be updated accordingly. In turn, that change will be made visible to the RootView, which will eventually redraw its contents and display the corresponding to the currentview‘s value view.

Menu working

Conclusion

What I demonstrated in this post is a solid way to navigate among views when they exist in the same hierarchy level. Probably that approach could get improved to suit more specific needs, but what I showed you today is a perfectly working solution. And as you read, it’s also easy to be implemented. So go ahead to adopt and adapt it in your own projects and work.

You can download the sample project demonstrated in this post from this link.

Stay Up To Date

Subscribe to my newsletter and get notifiied instantly when I post something new on SerialCoder.dev.

    We respect your privacy. Unsubscribe at any time.