Chapters

Hide chapters

SwiftUI by Tutorials

Fifth Edition · iOS 16, macOS 13 · Swift 5.8 · Xcode 14.2

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

13. Navigation
Written by Bill Morefield

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

It’s rare to find an app that can work with only a single view; most apps use many views and provide a way for the user to navigate between them smoothly. The navigation you design has to balance many needs:

  • You need to display data logically to the user.
  • You need to provide a consistent way to move between views.
  • You need to make it easy for the user to figure out how to perform a particular task.

SwiftUI provides a unified interface to manage navigation while also displaying data. SwiftUI 4.0 introduced significant changes to navigation for SwiftUI apps. In this chapter, you’ll explore building a navigation structure for an app using these new views.

Getting Started

Open the starter project for this chapter; you’ll find a very early version of a flight-data app for an airport. You’ll build out the navigation for this app. You would likely get the flight information from an external API in a real-world app. For this app, you’ll be using mock data.

To start, expand the Models folder in the app. Open FlightData.swift, and you’ll find the implementation of the mock data for this app. The FlightData class generates a schedule for fifteen days of flights with thirty flights per day starting with today’s date using the generateSchedule() method. The class uses a seeded random number generator to produce consistent flight data every time, with only the start date changing.

Now, open and examine FlightInformation.swift, which encapsulates information about flights. You’ll use this mock data throughout the following chapters.

Open WelcomeView.swift. The view includes a @StateObject named flightInfo that holds this mock data for the app.

Creating Navigation Views

Build and run the starter app. There’s a bare-bones implementation with a graphic and a single option to view the day’s flight status board.

Initial app
Ayokouq ofq

NavigationSplitView {
  Text("Sidebar")
} detail: {
  Text("Detail")
}
Split Navigation on an iPhone
Lgrij Nalomocual od id eFtona

Split Navigation on an iPad
Dgdap Rukasexoor il aq iMuv

enum FlightViewId: CaseIterable {
  case showFlightStatus
}

struct ViewButton: Identifiable {
  var id: FlightViewId
  var title: String
  var subtitle: String
}
var sidebarButtons: [ViewButton] {
  var buttons: [ViewButton] = []

  buttons.append(
    ViewButton(
      id: .showFlightStatus,
      title: "Flight Status",
      subtitle: "Departure and arrival information"
    )
  )

  return buttons
}
@State private var selectedView: FlightViewId?
// 1
List(sidebarButtons, selection: $selectedView) { button in
  // 2
  VStack {
    Text(button.title)
    Text(button.subtitle)
  }
}
// 3
.listStyle(.plain)
.navigationTitle("Mountain Airport")
Sidebar with First Navigation Link
Keripeg zupd Tanbt Locecuviuk Yush

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 the following:

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()
    )
  }
}
WelcomeButtonView(
  title: "Flight Status",
  subTitle: "Departure and Arrival Information"
)
WelcomeButtonView(
  title: button.title,
  subTitle: button.subtitle
)
Styled Sidebar Links
Lmhdin Qevikiv Pekvx

Building the Details View

Your details view will show the user a list of today’s flights. Open FlightStatusBoard.swift. At the top of the FlightStatusBoard struct, add a variable that you’ll use to pass in the list of flights for the day:

var flights: [FlightInformation]
List(flights, id: \.id) { flight in
  Text(flight.statusBoardName)
}
.navigationTitle("Today's Flight Status")
FlightStatusBoard(
  flights: FlightData.generateTestFlights(date: Date())
)
// 1
if let view = selectedView {
  // 2
  switch view {
  case .showFlightStatus:
    // 3
    FlightStatusBoard(flights: flightInfo.getDaysFlights(Date()))
  }
} else {
  // 4
  Text("Select an option in the sidebar.")
}
Flight list
Lmaqwj pigf

Building a NavigationStack

With the two-column navigation you’ve created, you can implement the details views separately from the initial view. Your Flight Status option displays a list of today’s flights. Next, you’ll show information on a flight when the user taps it on the list. To do so, you’ll wrap this list inside a new NavigationStack implemented inside the overall NavigationSplitView details view.

// 1
NavigationStack {
  List(flights, id: \.id) { flight in
    // 2
    NavigationLink(flight.statusBoardName, value: flight)
  }
  // 3
  .navigationDestination(
    // 4
    for: FlightInformation.self,
    // 5
    destination: { flight in
      FlightDetails(flight: flight)
    }
  )
  .navigationTitle("Today's Flight Status")
}
Flight list with arrow
Kxezgk zeqd nasd atjav

Flight details view
Yxilbk bifiunl buoz

Adding Items to the Navigation Bar

Creating a navigation view stack adds a navigation bar to each view. By default, the navigation bar only contains a button that returns to the previous view, except the first one. Beginning in iOS 14, the user can also long-press the back button to move anywhere up the view hierarchy in a single action.

Navigation Stack
Jowexiguor Ryepd

@State private var hidePast = false
var shownFlights: [FlightInformation] {
  hidePast ?
    flights.filter { $0.localTime >= Date() } :
    flights
}
List(shownFlights, id: \.id) { flight in
.navigationBarItems(
  trailing: Toggle("Hide Past", isOn: $hidePast)
)
NavigationStack {
  FlightStatusBoard(
    flights: FlightData.generateTestFlights(date: Date())
  )
}
Toggle
Gekpla

Navigating With Code

As you saw earlier, passing data down the navigation stack is simple. You can send the data as a read-only variable or pass a binding to allow the child view to make changes that are 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 back up can get complicated.

Navigation diagram
Kusevolioz quejman

import SwiftUI

class FlightNavigationInfo: ObservableObject {
  @Published var lastFlightId: Int?
}
@StateObject var lastFlightInfo = FlightNavigationInfo()
.environmentObject(lastFlightInfo)
case showLastFlight
if
  let flightId = lastFlightInfo.lastFlightId,
  let flight = flightInfo.getFlightById(flightId) {
  buttons.append(
    ViewButton(
      id: .showLastFlight,
      title: "\(flight.flightName)",
      subtitle: "The Last Flight You Viewed"
    )
  )
}
case .showLastFlight:
  if
    let flightId = lastFlightInfo.lastFlightId,
    let flight = flightInfo.getFlightById(flightId) {
    FlightDetails(flight: flight)
  }
@EnvironmentObject var lastFlightInfo: FlightNavigationInfo
.onAppear {
  lastFlightInfo.lastFlightId = flight.id
}
.environmentObject(FlightNavigationInfo())
.environmentObject(FlightNavigationInfo())
Selected flight in Welcome view
Modaspow ltibwh uz Wibhuga yaum

Navigating Using Code

The last flight option you added to the sidebar in the previous section brings up the details for the flight. While it works, it’s a good idea to let the user go back from these details to the same list of flights like when they select the Flight Status option. Until now, your navigation changes have come from user interaction. You’ll find times like this when you want to trigger navigation through your code. In this section, you’ll see how to interact with the navigation stack through code and use this to make this change.

var flightToShow: FlightInformation?
@State private var path: [FlightInformation] = []
NavigationStack(path: $path) {
.onAppear {
  if let flight = flightToShow {
    path.append(flight)
  }
}
FlightStatusBoard(
  flights: flightInfo.getDaysFlights(Date()),
  flightToShow: flight
)
Flight Details Showing Program Controlled Navigation
Cpulbd Zemaomw Bbocaps Fyaqjin Cingliwhid Wekehaquij

Using Tabbed Navigation

You’ve been using and building a hierarchical view stack with NavigationView up to this point in the app. Most apps use this structure, but there’s an alternative structure built around tabs. Tabs work well for content where the user wants to flip between options. In this app, you’ll implement tabs to show different versions of the flight status view.

struct FlightList: View {
  var flights: [FlightInformation]
  var flightToShow: FlightInformation?
  @State private var path: [FlightInformation] = []

  var body: some View {
    NavigationStack(path: $path) {
      List(flights, id: \.id) { flight in
        NavigationLink(flight.statusBoardName, value: flight)
      }
      .navigationDestination(
        for: FlightInformation.self,
        destination: { flight in
          FlightDetails(flight: flight)
        }
      )
    }
    .onAppear {
      if let flight = flightToShow {
        path.append(flight)
      }
    }
  }
}
// 1
TabView {
  // 2
  FlightList(
    flights: shownFlights.filter { $0.direction == .arrival }
  )
  // 3
  .tabItem {
    // 4
    Image("descending-airplane")
      .resizable()
    Text("Arrivals")
  }
  // 5
  FlightList(
    flights: shownFlights,
    flightToShow: flightToShow
  )
  .tabItem {
    Image(systemName: "airplane")
      .resizable()
    Text("All")
  }
  FlightList(
    flights: shownFlights.filter { $0.direction == .departure }
  )
  .tabItem {
    Image("ascending-airplane")
    Text("Departures")
  }
}
.navigationTitle("Today's Flight Status")
.navigationBarItems(
  trailing: Toggle("Hide Past", isOn: $hidePast)
)
Flight status with tabs
Vxispg zyupal nigb sisx

Setting Tabs

Remembering the last tab selected when the user returns to the view would be a nice addition. To do that, in FlightStatusBoard.swift, below the hidePast state variable, add the following line:

@AppStorage("FlightStatusCurrentTab") var selectedTab = 1
TabView(selection: $selectedTab) {
.tag(0)
.tag(1)
.tag(2)
.onAppear {
  if flightToShow != nil {
    selectedTab = 1
  }
}

Setting Tab Badges

SwiftUI 3.0 introduced controls that let you set a badge for each tab. This badge provides extra information to the user, but the available space limits the amount of data you can show. You’ll add a badge item to indicate the number of incoming and outgoing flights to the Flight Status and a short text badge showing the date.

.badge(shownFlights.filter { $0.direction == .arrival }.count)
.badge(shownFlights.filter { $0.direction == .departure }.count)
var shortDateString: String {
  let dateF = DateFormatter()
  dateF.timeStyle = .none
  dateF.dateFormat = "MMM d"
  return dateF.string(from: Date())
}
.badge(shortDateString)
Badges
Nozyot

Key Points

  • Starting in SwiftUI 4.0, navigation splits the declaration of a navigation action from the action to perform.
  • Split navigation can create a navigation structure with two or three columns. The framework will collapse the columns on small screen devices to appear identical to stack navigation.
  • Navigation stack creates a hierarchy of views. The user can move further into the stack and can back up from within the stack.
  • A NavigationLink shows a view that provides a value associated with the view. The navigationDestination modifier informs SwiftUI how to act when provided a value of a given type.
  • You apply changes to navigation views to controls in the stack and not to the navigation type itself.
  • You can access the view stack in your code by passing a binding to a mutable collection through the path parameter. In simple cases, this can be a collection of the type being used for navigation. If you need multiple types, you can use a NavigationPath.
  • Tab views display flat navigation that allows quick switching between the views.

Where to Go From Here?

To learn about migrating the older navigation types to the new ones introduced with SwiftUI 4.0, see Migrating to new navigation types at https://developer.apple.com/documentation/swiftui/migrating-to-new-navigation-types.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now