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.

Initial Starter App
Initial Starter App

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.

Split Navigation on an iPhone
Split Navigation on 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.

Split Navigation on an iPad
Split Navigation on an iPad

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:

  1. SwiftUI expects the sidebar to provide a list. You use the selection parameter to pass in the selectedView property. When the user taps a button from the list, SwiftUI will store the id property of the current ViewButton objects into the selectedView property. SwiftUI knows a change to the property passed to the selection parameter should cause state changes. If you change the property elsewhere, it’ll still trigger navigation.
  2. You display the button’s title and subtitle in a VStack.
  3. The plain list style removes most formatting. You’ll add some polish to it in the next section. You use the navigationTitle(_:) modifier to provide a title for the NavigationSplitView. Recall from the previous lesson that you call navigationTitle(_:) inside the navigation structure. The navigationTitle(_:) 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.

Sidebar with First Navigation Link
Sidebar with First Navigation Link

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.

Sidebar with Link on iPad
Sidebar with Link on iPad

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:

  1. Using maxWidth: .infinity sets the view to fill the available horizontal space.
  2. 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.

Styled Sidebar Links
Styled Sidebar Links

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.

Flight Status Board
Flight Status Board

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:

  1. Recall that in the List showing the buttons, you used the selection parameter to tell SwiftUI to store the id in the selectedView variable. You use a switch statement on that variable to display different content for each option.
  2. For the case where selectedView equals showFlightStatus, show the FlightStatusBoard view passing in the current flights for display.
  3. Recall you made selectedView optional. Until the user selects a view, the value will be nil. 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.

Split navigation on an iPhone changes between views.
Split navigation on an iPhone changes between views.

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.

The split navigation on an iPad.
The split navigation on an iPad.

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:

Navigation diagram
Navigation diagram

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.

See forum comments
Download course materials from Github
Previous: Introduction Next: Demo