NavigationView and NavigationLink: Basic Navigation in SwiftUI
Before NavigationStack
and NavigationSplitView
, SwiftUI introduced the NavigationView
and NavigationLink
structs. These two pieces work together to allow navigation between views in your application. The NavigationView
is used to wrap the content of your views, setting them up for subsequent navigation. The NavigationLink
does the actual work of assigning what content to navigate to and providing a UI component for the user to initiate that navigation. Let’s look at a quick example.
var body: some View { /// 1 NavigationView { Text("Primary View") NavigationLink( /// 2 destination: Text("Secondary View"), /// 3 label: { Text("Navigate") }) } }
Let’s break the above down:
- We encompass our content within a
NavigationView
, otherwise ourNavigationLink
will not function. - Within our
NavigationLink
we assign thedestination
. This is whatever content we want to navigate to. - Assigns a piece of content to act as a button for the navigation.
There are a number of ways to set up your NavigationLink
, so check the documentation for the right flavor for your own implementation. The above example is just one of the simplest ways to handle basic navigation.
The most notable thing about this compared to the way navigation was previously handled in UIKit is that all of our code is handled in a single location, within the SwiftUI view itself. This means we’ll have cleaner code in many instances, but also that any information we want to pass to subsequent screens must be available within the view we’re navigating from. This may seem a minor distinction, but it will be important later.
NavigationStack and NavigationSplitView: New to SwiftUI 4
NavigationStack
NavigationStack builds a list of views over a root view rather than defining each view individually. When a user interacts with a NavigationLink, a view is added to the top of the stack. The stack will always show the most recently added view that hasn’t been removed.To create a NavigationStack
:
var body: some View { NavigationStack { List(users) { user in NavigationLink(user.name, value: user) } .navigationDestination(for: User.self) { user in UserDetails(user: user) } } }
NavigationSplitView
is a special type of view that presents in two or three columns — particularly useful for tablets. Selections in the leading column (such as a menu) will control the presentation in the next columns (such as a content box).To create a NavigationSplitView
:
var body: some View { NavigationSplitView { List(model.users, selection: $userids) { user in Text(user.name) } } detail: { UserDetails(for: userids) } }
A developer can embed a NavigationStack
within a NavigationSplitView
column. When viewed in a small format device, columns will collapse.
Both NavigationStack
and NavigationSplitView
are newly introduced, but still utilize NavigationLink
. Apple provides migration directions for transitioning older navigation types to these newer navigation types.
Programmatic Navigation in SwiftUI
What happens when I need to await a response from an API call before navigating? What about waiting on the completion of an animation? Let’s say I need to perform validation on user fields before navigating? WHAT THEN!?!?!? Well, when setting up your NavigationLink
you do so with an isActive
parameter which will allow you to toggle the navigation. Let’s check out a quick example.
/// 1 @State var goWhenTrue: Bool = false var body: some View { NavigationView { Text("Primary View") /// 2 NavigationLink("Navigator", destination: Text("Subsequent View"), isActive: $goWhenTrue) } Button("Go Now") { /// 3 goWhenTrue = true }
- We need some variable to act on, and that variable must be bindable (@Binding, @State, etc) so that the view will re-render and navigate when it is updated.
- Here we set up the
NavigationLink
. The first parameter is a tag for the link itself and is not displayed in the UI. TheisActive
parameter is a boolean that controls whether the navigation executes. - Here we set the state variable to
true
, thus triggering the navigation in (2)
This can be set up in any number of different ways such as the variable being controlled by the view, a view model, a singleton, or any other point of reference within the application that the view can reference for the isActive
binding variable. This means we can not wait on other criteria or completions before navigation, but does that really solve all of our problems?
Programmatic Navigation Outside of Views
I mentioned above that all navigation must be configured within the SwiftUI view for navigation to be possible. We can trigger that navigation from other places using the steps outlined above, but what if our navigation is prompted from outside of the view hierarchy? This can be the case for Deep Linking, responding to an asynchronous event, or any number of other reasons (use your imagination). Unfortunately, there isn’t a good answer for this, but in the interest of scholarly pursuit, let’s see what we can do! Below is a potential workaround for this issue. We’ll start with our NavigationCoordinator
which stores our content before we perform navigation.
class NavigationCoordinator: ObservableObject { /// 1 fileprivate var screen: AnyView = AnyView(EmptyView()) ///2 @Published fileprivate var shouldNavigate: Bool = false ///3 func show<V: View>(_ view: V) { let wrapped = AnyView(view) screen = wrapped shouldNavigate = true } }
- This stores the content that we want to navigate to.
- Our binding variable for determining when we want to navigate.
- A helper method that handles the wrapping of our content and kicks off navigation automatically when assigned.
Next, we’ll look at our NavigationWrapper
which is implemented in any SwiftUI view that we want to be able to navigate from.
struct NavigationWrapper<Content>: View where Content: View { /// 1 @EnvironmentObject var coordinator: NavigationCoordinator /// 2 private let content: Content public init(@ViewBuilder content: () -> Content) { self.content = content() } var body: some View { NavigationView { /// 3 if coordinator.shouldNavigate { NavigationLink( destination: coordinator.screen, isActive: $coordinator.shouldNavigate, label: { content }) } else { content } } /// 4 .onDisappear(perform: { coordinator.shouldNavigate = false }) } }
- This is where we store our coordinator information for subsequent use during navigation.
- This is where we store the content that will be displayed on the view we’re navigating from. This is essentially your view body.
- Here we check to see if the coordinator should navigate. This will be checked whenever the environment object is updated and will trigger navigation when things have been set properly.
- Once we have successfully navigated away from this view, we want to set
shouldNavigate
to false to prevent the next view in the hierarchy from attempting navigation as well on load.
This is designed with the intent that you can use the above implementations to allow navigation from anywhere within your application to a new view, even outside of a SwiftUI view. It is a bit heavy-handed, as it requires that any scene you implement wrap its content in the NavigationWrapper
and pass the appropriate coordinator information to it. Beyond that, it is also by no means a “best practices” implementation as it can create additional overhead and is only necessary for specific instances.
Final Thoughts
At this time, it seems that programmatic navigation within SwiftUI is always going to come with certain caveats and additional considerations. Be sure to think through your navigation thoroughly and consider how you can centralize how your users move through your application.