Search

Navigation in SwiftUI 4: NavigationView, NavigationLink, NavigationStack, and NavigationSplitView

Logan Klein

6 min read

Jun 7, 2022

Navigation in SwiftUI 4: NavigationView, NavigationLink, NavigationStack, and NavigationSplitView

SwiftUI has changed a great many things about how developers create applications for iOS, and not just in the way we lay out our views. One area of significant impact is the way we navigate between scenes. 

Until recently, we used NavigationView and NavigationLink. In June of 2022, Apple introduced NavigationStack and NavigationSplitView. Developers now have multiple methods of navigating through scenes:

  • NavigationView (deprecated).
  • NavigationLink.
  • NavigationStack.
  • NavigationSplitView.
  • Programmatic navigation.

Below, we’ll cover the basics of navigation in SwiftUI 4.

Big Nerd Note: This article has been updated as of 9/22/2022 regarding the new features, NavigationStack and NavigationSplitView.

 

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:

  1. We encompass our content within a NavigationView, otherwise our NavigationLink will not function.
  2. Within our NavigationLink we assign the destination. This is whatever content we want to navigate to.
  3. 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

NavigationStackNavigationStack 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 NavigationSplitViewcolumn. 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
    }
  1. 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.
  2. Here we set up the NavigationLink. The first parameter is a tag for the link itself and is not displayed in the UI. The isActive parameter is a boolean that controls whether the navigation executes.
  3. 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
    }
}
  1. This stores the content that we want to navigate to.
  2. Our binding variable for determining when we want to navigate.
  3. 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
        })
    }
}
  1. This is where we store our coordinator information for subsequent use during navigation.
  2. This is where we store the content that will be displayed on the view we’re navigating from. This is essentially your view body.
  3. 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.
  4. 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.

Mark Dalrymple

Reviewer Big Nerd Ranch

MarkD is a long-time Unix and Mac developer, having worked at AOL, Google, and several start-ups over the years.  He’s the author of Advanced Mac OS X Programming: The Big Nerd Ranch Guide, over 100 blog posts for Big Nerd Ranch, and an occasional speaker at conferences. Believing in the power of community, he’s a co-founder of CocoaHeads, an international Mac and iPhone meetup, and runs the Pittsburgh PA chapter. In his spare time, he plays orchestral and swing band music.

Speak with a Nerd

Schedule a call today! Our team of Nerds are ready to help

Let's Talk

Related Posts

We are ready to discuss your needs.

Not applicable? Click here to schedule a call.

Stay in Touch WITH Big Nerd Ranch News