Passing Data Between Views in Hierarchical Navigation
First, you’ll create a class to add to the environment. Under the Models group, create a new file named FlightNavigationInfo.swift. Change the file to read:
import SwiftUI
class FlightNavigationInfo: ObservableObject {
@Published var lastFlightId: Int?
}
The single property will store the id of the last flight the user views. Now, you’ll add this to the parent navigation view.
Open WelcomeView.swift and, at the end of the variables at the top of the struct, add the following code:
@StateObject var lastFlightInfo = FlightNavigationInfo()
This line creates a StateObject you can now attach to the environment for the NavigationStack. At the closing brace of the detail closure, add the following code:
.environmentObject(lastFlightInfo)
This modifier adds the FlightNavigationInfo object to the environment for your navigation. You must add it to the NavigationSplitView and not to a view within it for the environment to flow through your view hierarchy.
Next, add a new case for the ButtonViewId enum by adding the following code to the end of it:
case showLastFlight
Now, add the following code to the sidebarButtons computed property just before the return statement:
if
let flightId = lastFlightInfo.lastFlightId,
let flight = flightInfo.getFlightById(flightId) {
buttons.append(
ViewButton(
id: .showLastFlight,
title: "\(flight.flightName)",
subtitle: "The Last Flight You Viewed"
)
)
}
This code attempts to unwrap lastFlightInfo.lastFlightId. If successful, it then uses the getFlightById(_:) method to get the flight for the flight id and attempts to unwrap that. If both succeed, you have the last flight the user viewed in the flight variable. You then add a button to the sidebar navigation using the new showLastFlight type with the name of the flight as the title.
You need to implement the details for this new navigation option. Inside the detail closure, find the switch view statement and add the following code before the default case:
case .showLastFlight:
if
let flightId = lastFlightInfo.lastFlightId,
let flight = flightInfo.getFlightById(flightId) {
FlightDetails(flight: flight)
}
This code works similarly to that inside the sidebarButtons computed property. You attempt to unwrap the lastFlightInfo.lastFlightId property. When that succeeds, you use it to get the last flight corresponding to that id. You then show the FlightDetails view passing that flight.
Last, you’ll set the value through the environment when the user views a flight’s details. Open FlightDetails.swift and add a reference to the environment object to the view after the flight property:
@EnvironmentObject var lastFlightInfo: FlightNavigationInfo
With this reference to the view’s environment, add the following code after the closing brace for the ZStack:
.onAppear {
lastFlightInfo.lastFlightId = flight.id
}
Any code in the onAppear(_) closure runs when the view appears. In this case, when SwiftUI renders the ZStack, it executes the code and stores the id for this flight in the environment. When the user returns to the root welcome view, that view will read the value and show the button.
The change will cause the live view to crash because it doesn’t know about the new environment object. To fix this, you provide an environment object for the preview. Add the following modifier after the NavigationStack in the #Preview:
.environmentObject(FlightNavigationInfo())
Run the app. The second button doesn’t show since the identifier is initially nil. Tap Flight Status and then tap any flight. Now go back to the sidebar view, and you’ll see the new button listing the flight you just viewed.
Now that you’ve explored two-column navigation, you’ll adapt this app for three-column navigation.
Three-Column Navigation
Your current navigation structure contains a sidebar with the app’s top-level navigation structure. For the Flight Status view, you display a NavigationStack for the details view. In this case, you know that your structure contains three columns: the top-level navigation, the list of flights, and the details for a single flight. That makes it a good candidate for the three-column version of the NavigationSplitView.
You’ll implement a content parameter to the NavigationSplitView to convert the two-column view to a three-column view. The process resembles what you did for the two-column navigation. Instead of using the NavigationStack, NavigationLink and navigationDestination views and modifiers, you’ll change the flight list to use a bindable property.
Open WelcomeView.swift and add the following code after the selectedView property:
@State private var selectedFlight: Int?
This property provides an optional Int to store a flight when chosen.
Next, you’ll move the current detail parameter to the content parameter. The content parameter provides the second column for the two-column view. To do this, change the detail parameter in your view to content.
To provide the detail parameter containing the third column of the three-column view, add the following code after the content section on the line with its closing brace:
} detail: {
if let flightId = selectedFlight,
let flight = FlightData().getFlightById(flightId) {
FlightDetails(flight: flight)
}
}
This code should look familiar. You attempt to unwrap the flightId property you added above. If that succeeds, you attempt to get the flight with that id using the FlightData.getFlightById(_:) method. If both succeed, you display the flight information and pass in that flight to the view.
Before this works, you must change the content section of the second column. It currently displays a self-contained NavigationStack to perform this work. Open FlightList.swift inside the FlightStatusBoard group. Notice the following line:
@Binding var selectedFlight: Int?
This variable provides a bindable property similar to the selectedView property you can use to track which item in the list the user selects. This ties into the List in this view:
List(shownFlights, selection: $selectedFlight) { flight in
Now, you use that variable to store the flight’s id when the user selects a row.
You can now change the second column to use the FlightList view. Open WelcomeView.swift and change the code in the content parameter under the showFlightStatus option in the switch to:
FlightList(
flights: flightInfo.flights,
selectedFlight: $selectedFlight
)
This code will pass the selectedFlight as a binding to the FlightList view. When the user selects a flight in that view, the change will flow back to this view through the binding.
Using three columns negates the need for the FlightNavigationInfo class you used earlier. To see this, run the app. Tap Flight Status, then tap any flight. Go back to the sidebar, and the button for the last flight appears. Repeat the process and select a different flight. When you return to the sidebar, you’ll see it still shows the name of the first flight.
The FlightNavigationInfo environment object is only updated when the FlightDetails view appears. In a three-column navigation layout, it’ll only be called the first time the user selects a flight because SwiftUI reuses the FlightDetails in the third column on a larger device such as an iPad.
However, the answer is that you already have this information in the sidebar column in the selectedFlight property. Go to the sidebarButtons computed property and change:
let flightId = lastFlightInfo.lastFlightId,
To:
let flightId = selectedFlight,
Now check the selectedFlight property tied to the third column. This will update the button anytime the selection changes.
Build and run the app. You won’t notice a visual difference on an iPhone since the individual columns all render as full-screen views. However, on the iPad, you’ll see all three columns displayed.
There are a few complications with the new column. Notice that the Last Flight button only uses two of the three columns. You might wonder what’s happening in that third column, especially if you’re only testing on an iPhone.
Run the app on an iPad, tap Flight Status, and then tap any flight. Now, go back to the sidebar and tap the new button.
Note that the second column now appears as the sidebar did before. You can go back to the sidebar using the Back button in the navigation bar of the second column.
SwiftUI doesn’t provide a native way to mix two and three-column navigation. You have some control over column visibility through the optional columnVisibility parameter to the NavigationSplitView initializer. Still, none provide a way to hide the details column in the three-column view, and you’ll need to build a custom solution with control logic to mix the number of columns. In most cases, as you did earlier in this lesson, you’ll find it easier to keep a two-column layout and simulate the third column within the content column.