Chapters

Hide chapters

SwiftUI by Tutorials

Fourth Edition · iOS 15, macOS 12 · Swift 5.5 · Xcode 13.1

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, and 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. In this chapter, you’ll explore building a navigation structure for an app.

Getting started

Open the starter project for this chapter; you’ll find a very early version of a flight-data app for an airport. In this chapter, you will build out the navigation for this app. In a real-world app, you would likely get the flight information from an API through Combine. For this app, though, 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 a consistent set of flight data every time with only the start date changing.

Also open and examine FlightInformation.swift, which encapsulates information about flights. You’ll be using this mock data through the next several chapters while building out this app.

Open WelcomeView.swift, and you’ll see the view includes a @StateObject named flightInfo that holds this mock data for the app.

Navigating through a SwiftUI app

When designing the navigation for your SwiftUI app, you must create a navigation pattern that helps the user move confidently through the app and intuitively perform tasks. Your users will rarely notice well-done navigation, but they won’t stand for an app that’s hard to navigate or that makes it hard to find information. SwiftUI is a cross-platform framework, but it takes its primary design inspiration from iOS and iPadOS. Therefore, SwiftUI integrates patterns and design guidelines that are common on those platforms.

Flat navigation
Rpol qefireqiut

Hierarchical navigation
Piocucvvulam nuboronaus

Creating navigation views

Build and run the starter app. You’ll see a bare-bones implementation with a graphic and a single option to view the day’s flight status board. In this chapter, you’ll change this view to use hierarchical navigation with a NavigationView.

Initial app
Ibayiup enp

// 1
NavigationView {
  ZStack(alignment: .topLeading) {
    // 2
    Image("welcome-background")
      .resizable()
      .frame(height: 250)
    VStack(alignment: .leading) {
      // 3
      NavigationLink(
        // 4
        destination: FlightStatusBoard()
      ) {
        // 5
        Text("Flight Status")
      }
      Spacer()
    }.font(.title)
    .foregroundColor(.white)
    .padding()
  // 6
  }.navigationBarTitle("Mountain Airport")
  // End Navigation View
}
Navigation title
Xolitafoek zugbo

Blank screen iPad
Spazy gxzuah uPul

.navigationViewStyle(StackNavigationViewStyle())
Correct look in iPad
Temruld qeeh ay eMip

Polishing the links

Before moving to the child navigation views, you’ll improve the button’s look 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: "Flight Status",
  subTitle: "Departure and arrival information"
)
Navigation link
Navenuyuov rahf

Using navigation links

You’ll first create a view that implements the first option from the WelcomeView, providing more detailed information about today’s flight to the user.

var flights: [FlightInformation]
var body: some View {
  List(flights, id: \.id) { flight in
    Text(flight.statusBoardName)
  }.navigationBarTitle("Flight Status")
}
FlightStatusBoard(
  flights: FlightData.generateTestFlights(date: Date())
)
NavigationLink(
  destination: FlightStatusBoard(
    flights: flightInfo.getDaysFlights(Date()))
) {
  WelcomeButtonView(
    title: "Flight Status",
    subTitle: "Departure and arrival information"
  )
}
static var previews: some View {
  NavigationView {
    FlightStatusBoard(
      flights: FlightData.generateTestFlights(date: Date())
    )
  }
}
Flight list
Ysohbw wetz

Extending the hierarchy

Your navigation follows the flow from more general information to more specific information. Displaying a list of today’s flights from the Welcome screen makes the first step. Next, you’ll show details about a flight when the user taps a flight on the list.

List(flights, id: \.id) { flight in
  NavigationLink(
    flight.statusBoardName,
    destination: FlightDetails(flight: flight)
  )
}.navigationBarTitle("Flight Status")
Flight list with arrow
Ldopfj satl xeyz uwlov

Flight details view
Kyargp bobaejq daiy

Adding items to the navigation bar

Creating a navigation view stack adds a navigation bar to each view. By default, the navigation bar contains only a button that links back to the previous view (for all views 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.

@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)
)
Toggle
Tiycmo

Navigation via code

The default navigation link responds to a user’s action, turning the view into a button. When the user taps that button, the movement to the next view triggers. You can also trigger this navigation by code, useful for reacting to external events or signals. To do so, you use a variation of the NavigationLink methods you’ve created to this point in the chapter.

NavigationLink(
  destination: FlightDetails(flight: flightInfo.flights.first!),
  // 1
  isActive: $showNextFlight
  // 2
) { }
@State var showNextFlight = false
Button(action: {
  showNextFlight = true
}) {
  WelcomeButtonView(
    title: "First Flight",
    subTitle: "Detail for First Flight of the Day"
  )
}
First flight of the day
Weglw mjehfc as gxe nuc

Sharing the environment

As you saw earlier, it’s simple to pass data down the navigation stack. You can send the data as a read-only variable or pass a binding to allow the child view to make changes 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
Riguguyaug biamgel

import SwiftUI

class FlightNavigationInfo: ObservableObject {
  @Published var lastFlightId: Int?
}
@StateObject var lastFlightInfo = FlightNavigationInfo()
.environmentObject(lastFlightInfo)
// 1
if
  let id = lastFlightInfo.lastFlightId,
  let lastFlight = flightInfo.getFlightById(id) {
  Button(action: {
    // 2
    showNextFlight = true
  }) {
    WelcomeButtonView(
    // 3
      title: "Last Flight \(lastFlight.flightName)",
      subTitle: "Show Next Flight Departing or Arriving at Airport"
    )
  }
}
if
  let id = lastFlightInfo.lastFlightId,
  let lastFlight = flightInfo.getFlightById(id) {
  NavigationLink(
    destination: FlightDetails(flight: lastFlight),
    isActive: $showNextFlight
  ) { }
}
@EnvironmentObject var lastFlightInfo: FlightNavigationInfo
.onAppear {
  lastFlightInfo.lastFlightId = flight.id
}
.environmentObject(FlightNavigationInfo())
Selected flight in Welcome view
Rufojqir dbodzl aj Niwboga boat

Using tabbed navigation

You’ve been using and building a hierarchical view stack with NavigationView to this point in the app. Most apps use this structure, but there is 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 body: some View {
    List(flights, id: \.id) { flight in
      NavigationLink(
        flight.statusBoardName,
        destination: FlightDetails(flight: flight)
      )
    }
  }
}
// 1
TabView {
  // 2
  FlightList(
    flights: shownFlights.filter { $0.direction == .arrival }
  )
  // 3
  .tabItem {
    // 4
    Image("descending-airplane")
      .resizable()
    Text("Arrivals")
  }
  FlightList(
    flights: shownFlights
  )
  .tabItem {
    Image(systemName: "airplane")
      .resizable()
    Text("All")
  }
  FlightList(
    flights: shownFlights.filter { $0.direction == .departure }
  )
  .tabItem {
    Image("ascending-airplane")
    Text("Departures")
  }
}.navigationTitle("Flight Status")
.navigationBarItems(
  trailing: Toggle("Hide Past", isOn: $hidePast)
)
Flight status with tabs
Fwuhlg gcopop recs xold

Setting tabs

It would be a nice addition to remember the last tab selected when the user returns to the view. Still in FlightStatusBoard.swift, below the hidePast state variable add the following line:

@AppStorage("FlightStatusCurrentTab") var selectedTab = 1
// 1
TabView(selection: $selectedTab) {
  FlightList(
    flights: shownFlights.filter { $0.direction == .arrival }
  ).tabItem {
    Image("descending-airplane")
      .resizable()
    Text("Arrivals")
    // 2
  }
  .tag(0)
  FlightList(
    flights: shownFlights
  ).tabItem {
    Image(systemName: "airplane")
      .resizable()
    Text("All")
  }
  .tag(1)
  FlightList(
    flights: shownFlights.filter { $0.direction == .departure }
  ).tabItem {
    Image("ascending-airplane")
    Text("Departures")
  }
  .tag(2)
}.navigationTitle("Flight Status")
.navigationBarItems(
  trailing: Toggle("Hide Past", isOn: $hidePast)
)

Setting tab badges

SwiftUI 3 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 show the number of flights for incoming and outgoing flights to the Flight Status along with 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
Teygeh

Key points

  • App navigation generally combines a mix of flat and hierarchical flows between views.
  • Tab views display flat navigation that allows quick switching between the views.
  • Navigation views create a hierarchy of views as a view stack. The user can move further into the stack and can back up from within the stack.
  • A navigation link connects a view to the next view in the view stack.
  • You should only have one NavigationView in a view stack. Views that follow should inherit the existing navigation view.
  • You apply changes to the navigation view stack to controls in the stack, and not to the NavigationView itself.

Where to go from here?

The first stop when looking for information on user interfaces on Apple platforms should be the Human Interface Guidelines on Navigation for iOS, watchOS and tvOS:

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