Everything About SwiftUI Navigations

Tharindu Ramesh Ketipearachchi
6 min readJun 3, 2023

--

SwiftUI has many different components for navigations. Earlier, it was only NavigationView & NavigationLink. But in June 2022 Apple has introduced couple of new components as well. Now developers have different options based on different situations.

  1. NavigationView(deprecated)
  2. NavigationLink
  3. NavigationSplitView
  4. NavigationStack
  5. Programmatic Navigation

Today, we are going to discuss one by one with examples.

Basic Navigations in SwiftUI — NavigationView & NavigationLink

Initially the navigations of SwiftUI being basically handled through the NavigationView & the NavigationLink. NavigationView allows us to push and pop screens as UINavigationController on UIKit.

First, whatever you are trying to display, you need to wrap it with NavigationView. In normal, navigation views should be the top-level element on your view. The content inside the navigation view to be considered as the content of particular view or the screen that you are trying to present.

struct ContentView: View {
var body: some View {
NavigationView {
Text("Hello, World!")
}
}
}

But when you are using it inside a TabView, then the navigation view should be inside the tabview. We can do some stylings to navigation view such as setting up the titles as well. But when you do it you have to do it within the navigation view.

NavigationView {
VStack {
Text("Welcome Home!")
}
.navigationTitle("Home")
}

The idea here is, whatever the content wrapped inside navigation view is considered as a screen. In above example, Vstack represents our HomeScreen. When you are setting a screen title, you need to set it to your screen, not to the navigation view. That’s why the title should be set inside the navigation view.

NavigationView alone itself not capable enough to handle the navigations. We need NavigationLink to complete our navigation. Navigation view present new screens using NavigationLink, which can be triggered by a tapping of the user.

NavigationView {
VStack {
NavigationLink(destination: SettingsView()) {
Text("Go to Settings")
}
}
.navigationTitle("Home")
}

struct SettingsView: View {
var body: some View {
VStack {
Text("Settings")
}
}
}

Here we have added a navigation link to our home screen. Once user tap that, the SettingsView will be presented to the user. The navigation link is basically working as a button and you can add a text label to the links as well.

Note : From iOS 16 onwards NavigationView has been deprecated. Now the SwiftUI navigations are totally handled through the NavigationStack & NavigationSplitView.

NavigationSplitView

NavigationSplitView is mostly using with wide length devices such as iPads and desktops, also the landscape mode of iPhones. But automatically collapses to a NavigationStack style layout when space is limited. Here, we can display multiple screens at the same time on the screen by dividing screen into multiple columns. This is really useful with navigations with multi level list views.

The simplest form of NavigationSplitView , you can provide your side menu as its first trailing closure, and relevant screen as its second like follows.

NavigationSplitView {
Text("SideMenu")
} detail: {
Text("Detail View")
}

Let say, we fetch users array from an API, show their names as a list and once user clicked the name, we want to show user details screen,

var body: some View {
NavigationSplitView {
List(users, selection: $userids) { user in
Text(user.name)
}
} detail: {
UserDetails(for: userids)
}
}

If you want to go for another level, NavigationSplitView allows us to add 3rd view to our layout as well. You can do it as follows

NavigationSplitView {
Text("Sidebar")
} content: {
Text("Primary View")
} detail: {
Text("Detail View")
}

NavigationStack & NavigationPath

NavigationStack is the component that SwiftUI provided for stack based navigations. This is pretty similar to UINavigationController on the UIKit.

In UINavigationController you can add UIViewControllers to the view controllers stack. But SwiftUI introduced a different approach here. First we need to define an array of any type of objects and then we have append elements to that array, instead of adding new views to NavigationStack.

struct ContentView: View {
@State private var path: [Item] = []

var body: some View {
NavigationStack(path: $path) {
HomeView()
.navigationDestination(for: Item.self) { item in
ItemDetailsView(item: item)
}
}
}
}

As you have seen in above code, you can pass an array as a @Stateobject to NavigationStack. Once you append an item to this array, It will be adding a new screen to theNavigationStack, just like we push a new ViewController to UINavigationController . This item can be passed to the particular screen using the navigationDestination(for:)method and can be accessed from that particular screen as well.

The problem with this approach is in reality, we don’t pass same type of objects to all the screens in our app. We have to pass different type of objects to different screens with different views. That’s why Apple has introduced new type called NavigationPath . This is a type that allows us to store any type of data. Using this we can append heterogeneous elements to our path array. The only concern is that all those element types should to conform to Hashable protocol. Now we can define our path variable as follows.

@State private var path = NavigationPath()

Now you can append different type of elements to path array and will be able to push different views with different data.

struct ContentView: View {
@State private var path = NavigationPath()

var body: some View {
NavigationStack(path: $path) {
HomeView()
.navigationDestination(for: User.self) { user in
UserDetailsView(user: user)
}
.navigationDestination(for: Item.self) { item in
ItemDetailsView(item: item)
}
.navigationDestination(for: Cart.self) { cart in
CartView(cart: cart)
}
}
}
}

Here, you can see that if we want to display UserDetailsView, we just need to append a user object to our path array. Then it’ll pass the user object to UserDetailsView and present the view. Same way if we need to display the UserDetailsView, we just need to append an item object to path array, This will pushes a ItemDetailsView to our navigation stack. This wasn’t possible earlier with the SwiftUI NavigationStack. This latest NavigationPath API has been introduced with iOS 16.0

Programmatic Navigations

Almost all the navigations methods that we have discussed above were triggered through a user action. A button click or link click. But there are many situations, we have to handle these navigations programmatically. As an example, let say I need to call a web service, after the web service response returns, then we need to navigate user to another screen.

SwiftUI has provided an extra argument for NavigationLink to achieve this purpose.

@State var isDataFetched: Bool = false

var body: some View {
NavigationView {
Text("Home")
NavigationLink("User",
destination: Text("User Details"),
isActive: $isDataFetched)
}

func fetchUserDetails() {
isDataFetched = true
}

Here you can see we have passed a State boolean value to isActive argument. It was initially set to false and make it true once the API call got completed. In this way we can enable these navigations programmatically.

Programmatic Navigation Outside of Views

In SwiftUI, Navigations and view is hardly bound together. Unfortunately SwiftUI doesn’t provide much facilities to handle our navigations outside from views. But in many occasions we have to handle our navigations outside the views, such as loading the views from DeepLinks, responding to asynchronous events.

This can be achieved through a work-around called NavigationCoordinator. Here we are using the common Coordinator pattern here. We define a NavigationCoordinator to our applications as an observable object and handle all the navigations from here.

We can improve this method to achieve MVVM-C architecture from SwiftUI as well. This is pretty much similar to what we used to do with UIKit MVVM-C. I have wrote separate article about this. Here, you’ll be able learn how to implement flow coordinator based modularised navigations with SwiftUI.

--

--

Tharindu Ramesh Ketipearachchi

Technical Lead (Swift, Objective C, Flutter, react-native) | iOS Developer | Mobile Development Lecturer |MSc in CS, BSc in CS (Col)