Hierarchical Navigation Structures
Building Hierarchical Navigation Structures
Hierarchical navigation gives users options at the top with a deeper structure underneath. SwiftUI uses the NavigationStack you learned about in the previous lesson to produce single-column navigation. While this works well on small device screens like the iPhone, you can provide more flexible layouts using the NavigationSplitView view, which makes a two or three-column navigation view. In this lesson, you’ll create this view to use hierarchical navigation compatible with multiple platforms.
The NavigationSplitView supports a split-view interface on larger devices, separating the app’s views into separate panes. One view provides the top-level navigation and remains static, while the second and optional third views change as the user navigates through the view stack. On smaller screens, like the iPhone, it falls back and produces a single-column view identical to the NavigationStack. Using a NavigationSplitView in a cross-platform app will make it easier to adapt to multiple screen sizes.
Open the starter app for this project. Build and run it. You’ll see a bare-bones implementation of top-level navigation with a graphic and a single non-working option to view the day’s flight status board.
Open WelcomeView.swift and replace the view body with:
NavigationSplitView {
Text("Sidebar")
} detail: {
Text("Detail")
}
You use the init(sidebar:detail:) initializer of NavigationSplitView to create a view with two columns. You could create a third column using the init(sidebar:content:detail:) initializer, but for now, you’ll focus on a two-column layout. For the moment, you’ve placed a single text view inside both columns.
Build and run the app on an iPhone simulator. The view inside the view closure fills the entire screen for an iPhone.
Build and run the app on a larger device like an iPad simulator. You’ll see both columns here. If you don’t see the sidebar initially, tap the Show Sidebar button at the top left corner of the screen or make a sliding gesture from the left side of the screen.
Preparing the Sidebar Buttons
With this structure in place, you can fill in the two columns. First, create the sidebar list that will drive the navigation. Above the WelcomeView implementation, add the following code:
enum ButtonViewId: CaseIterable {
case showFlightStatus
}
struct ViewButton: Identifiable {
var id: ButtonViewId
var title: String
var subtitle: String
}
This code defines a new ButtonViewId enumerable, which implements the CaseIterable protocol. Initially, it’ll have only a single case, showFlightStatus. You then specify the ViewButton struct. This struct contains three properties: an id, title, and subtitle. You’ll use these properties to define the buttons in the sidebar. The struct also implements the Identifiable protocol, which makes it easier to use inside a List. For now, know that the id property fulfills the protocol.
Now, add the following code inside the WelcomeView struct before the body of the view:
var sidebarButtons: [ViewButton] {
var buttons: [ViewButton] = []
buttons.append(
ViewButton(
id: .showFlightStatus,
title: "Flight Status",
subtitle: "Departure and arrival information"
)
)
return buttons
}
This computed property will provide an array of ViewButton structs that define the options for the sidebar. This design makes it easy to add more buttons as you expand the Mountain Airport app.
There’s one more step before you build the sidebar. Add the following property after flightInfo inside the WelcomeView:
@State private var selectedView: ButtonViewId?
This property will store the ButtonViewId when the user taps an item in the List. You use an optional type to handle the case before the user taps a button.
Creating the Sidebar
Now, create the list for the sidebar. Inside the view body, replace the Text("Sidebar") view with:
// 1
List(sidebarButtons, selection: $selectedView) { button in
// 2
VStack {
Text(button.title)
Text(button.subtitle)
}
}
// 3
.listStyle(.plain)
.navigationTitle("Mountain Airport")
Here’s how this sets up the sidebar:
- SwiftUI expects the sidebar to provide a list. You use the
selectionparameter to pass in theselectedViewproperty. When the user taps a button from the list, SwiftUI will store theidproperty of the currentViewButtonobjects into theselectedViewproperty. SwiftUI knows a change to the property passed to theselectionparameter should cause state changes. If you change the property elsewhere, it’ll still trigger navigation. - You display the button’s title and subtitle in a
VStack. - The
plainlist style removes most formatting. You’ll add some polish to it in the next section. You use thenavigationTitle(_:)modifier to provide a title for theNavigationSplitView. Recall from the previous lesson that you callnavigationTitle(_:)inside the navigation structure. ThenavigationTitle(_:)modifier locates the navigation view for the attached control and adjusts the title accordingly.
Build and run the app. On a small screen, the title appears above the list, while the list shows your single navigation item. If you tap this item, you’ll navigate to the detail screen showing only the static Text view.
On a larger screen, you’ll see that the sidebar matches the small screen display while the details show the static Text view. In the next section, you’ll apply some styling to the list.
Polishing the Links
Before moving to the details view, you’ll improve the button’s appearance from the current plain text. Create a new SwiftUI View named WelcomeButtonView.swift. Replace the default view with:
struct WelcomeButtonView: View {
var title: String
var subTitle: String
var body: some View {
VStack(alignment: .leading) {
Text(title)
.font(.title)
.foregroundColor(.white)
Text(subTitle)
.font(.subheadline)
.foregroundColor(.white)
}.padding()
// 1
.frame(maxWidth: .infinity, alignment: .leading)
// 2
.background(
Image("link-pattern")
.resizable()
.clipped()
)
}
}
Here are a couple of things to note:
- Using
maxWidth: .infinitysets the view to fill the available horizontal space. - You also use the
background(_:)modifier to provide an image background for the text.
This change provides a more visually appealing view to replace the simple text link. It also provides a short description to accompany each menu option.
Change the content of the preview to provide default data:
WelcomeButtonView(
title: "Flight Status",
subTitle: "Departure and Arrival Information"
)
Go back to WelcomeView.swift. Replace the current VStack view under // 2 with:
WelcomeButtonView(
title: button.title,
subTitle: button.subtitle
)
.listRowSeparator(.hidden)
You use the listRowSeparator(_:edges:) modifier to hide the separator lines for each item in the sidebar.
Run the app to see your new sidebar link.
Having improved the look of your sidebar, you’ll now put this list to work and implement the details view.
Showing the Details
Open FlightStatusBoard.swift inside the FlightStatusBoard group. The contents should look familiar since it’s your WelcomeView.swift from the end of lesson one with a few changes. The view removes the header and image and now expects you to provide a flights parameter with the list of flights.
To integrate the details view with the sidebar, go back to WelcomeView.swift*, then change the Text("Detail") view to:
// 1
switch selectedView {
// 2
case .showFlightStatus:
FlightStatusBoard(flights: flightInfo.flights)
// 3
default:
Text("Please select an option from the sidebar")
}
Here’s how this code works:
- Recall that in the
Listshowing the buttons, you used theselectionparameter to tell SwiftUI to store theidin theselectedViewvariable. You use aswitchstatement on that variable to display different content for each option. - For the case where
selectedViewequalsshowFlightStatus, show theFlightStatusBoardview passing in the current flights for display. - Recall you made
selectedViewoptional. Until the user selects a view, the value will benil. In that case, you display text asking the user to choose one of the options.
Run the app on an iPhone, then tap the Flight Status button. You’ll see the Flight Status Board appear in the details view. The result on a smaller screen device looks much like the NavigationStack used in lesson one.
On an iPad’s larger screen, you’ll initially see the default text because you haven’t selected any buttons from the side menu. Tap the Flight Status button, and you’ll see the Flight Status Board next to it in the details view. Note that the sidebar floats on top of the text in the details view when displayed.
Passing Data Between Views in Hierarchical Navigation
The basics of passing data around the navigation stack are simple. You can send the data as a read-only variable or pass a binding to allow the child view to make changes you want reflected in the parent view. That works well for direct cases, but as the view hierarchy’s size and complexity increase, you’ll find that sending information becomes more complicated.
Reusing views complicates this process since you can have multiple paths to the same view. In these cases, you could end up having to pass parameters solely to pass data between other views:
Fortunately, there’s a better way. A SwiftUI view automatically shares its environment with any view below it in the view hierarchy. This feature lets you put anything into the environment. You can then access or modify that data within any other view in the hierarchy.
Now, you’ll update the app to use this ability to save the most recent flight a user viewed and show that in place of the first flight from the previous section. You’ll implement this in the video portion of this lesson.